Compare commits
215 Commits
bug/rememb
...
v0.36.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
blank_issues_enabled: true
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: 💬 Discord Server
|
- 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
|
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.
|
adjusting the format.
|
||||||
|
|
||||||
Usage:
|
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 copy
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
@@ -18,6 +19,10 @@ import argparse
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
# Maximum size for properties files (e.g., 200 KB)
|
||||||
|
MAX_FILE_SIZE = 200 * 1024
|
||||||
|
|
||||||
|
|
||||||
def parse_properties_file(file_path):
|
def parse_properties_file(file_path):
|
||||||
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
|
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
|
||||||
properties_list = []
|
properties_list = []
|
||||||
@@ -95,7 +100,7 @@ def write_json_file(file_path, updated_properties):
|
|||||||
def update_missing_keys(reference_file, file_list, branch=""):
|
def update_missing_keys(reference_file, file_list, branch=""):
|
||||||
reference_properties = parse_properties_file(reference_file)
|
reference_properties = parse_properties_file(reference_file)
|
||||||
for file_path in file_list:
|
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 (
|
if (
|
||||||
basename_current_file == os.path.basename(reference_file)
|
basename_current_file == os.path.basename(reference_file)
|
||||||
or not file_path.endswith(".properties")
|
or not file_path.endswith(".properties")
|
||||||
@@ -103,7 +108,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
current_properties = parse_properties_file(branch + file_path)
|
current_properties = parse_properties_file(os.path.join(branch, file_path))
|
||||||
updated_properties = []
|
updated_properties = []
|
||||||
for ref_entry in reference_properties:
|
for ref_entry in reference_properties:
|
||||||
ref_entry_copy = copy.deepcopy(ref_entry)
|
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"]:
|
if ref_entry_copy["key"] == current_entry["key"]:
|
||||||
ref_entry_copy["value"] = current_entry["value"]
|
ref_entry_copy["value"] = current_entry["value"]
|
||||||
updated_properties.append(ref_entry_copy)
|
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):
|
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):
|
def read_properties(file_path):
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||||
return file.read().splitlines()
|
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]
|
reference_branch = reference_file.split("/")[0]
|
||||||
basename_reference_file = os.path.basename(reference_file)
|
basename_reference_file = os.path.basename(reference_file)
|
||||||
|
|
||||||
report = []
|
report = []
|
||||||
report.append(
|
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||||
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
|
|
||||||
)
|
|
||||||
reference_lines = read_properties(reference_file)
|
reference_lines = read_properties(reference_file)
|
||||||
has_differences = False
|
has_differences = False
|
||||||
|
|
||||||
only_reference_file = True
|
only_reference_file = True
|
||||||
|
|
||||||
for file_path in file_list:
|
file_arr = file_list
|
||||||
basename_current_file = os.path.basename(branch + "/" + file_path)
|
|
||||||
|
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 (
|
if (
|
||||||
basename_current_file == basename_reference_file
|
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 file_path.endswith(".properties")
|
||||||
or not basename_current_file.startswith("messages_")
|
or not basename_current_file.startswith("messages_")
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
only_reference_file = False
|
only_reference_file = False
|
||||||
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
|
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
|
||||||
current_lines = read_properties(branch + "/" + file_path)
|
current_lines = read_properties(os.path.join(branch, file_path))
|
||||||
reference_line_count = len(reference_lines)
|
reference_line_count = len(reference_lines)
|
||||||
current_line_count = len(current_lines)
|
current_line_count = len(current_lines)
|
||||||
|
|
||||||
if reference_line_count != current_line_count:
|
if reference_line_count != current_line_count:
|
||||||
report.append("")
|
report.append("")
|
||||||
report.append("- **Test 1 Status:** ❌ Failed")
|
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||||
|
report.append(" - **Issue:**")
|
||||||
has_differences = True
|
has_differences = True
|
||||||
if reference_line_count > current_line_count:
|
if reference_line_count > current_line_count:
|
||||||
report.append(
|
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:
|
elif reference_line_count < current_line_count:
|
||||||
report.append(
|
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:
|
else:
|
||||||
report.append("- **Test 1 Status:** ✅ Passed")
|
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||||
|
|
||||||
# Check for missing or extra keys
|
# Check for missing or extra keys
|
||||||
current_keys = []
|
current_keys = []
|
||||||
@@ -192,32 +216,42 @@ def check_for_differences(reference_file, file_list, branch):
|
|||||||
has_differences = True
|
has_differences = True
|
||||||
missing_keys_str = "`, `".join(missing_keys_list)
|
missing_keys_str = "`, `".join(missing_keys_list)
|
||||||
extra_keys_str = "`, `".join(extra_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:
|
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(
|
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:
|
if extra_keys_list:
|
||||||
report.append(
|
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:
|
else:
|
||||||
report.append("- **Test 2 Status:** ✅ Passed")
|
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||||
# if has_differences:
|
|
||||||
# report.append("")
|
|
||||||
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
|
|
||||||
report.append("")
|
report.append("")
|
||||||
report.append("---")
|
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:
|
if has_differences:
|
||||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
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:
|
else:
|
||||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
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:
|
if not only_reference_file:
|
||||||
print("\n".join(report))
|
print("\n".join(report))
|
||||||
@@ -225,6 +259,11 @@ def check_for_differences(reference_file, file_list, branch):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||||
|
parser.add_argument(
|
||||||
|
"--actor",
|
||||||
|
required=False,
|
||||||
|
help="Actor from PR.",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--reference-file",
|
"--reference-file",
|
||||||
required=True,
|
required=True,
|
||||||
@@ -244,11 +283,21 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
args = parser.parse_args()
|
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
|
file_list = args.files
|
||||||
if file_list is None:
|
if file_list is None:
|
||||||
file_list = glob.glob(
|
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)
|
update_missing_keys(args.reference_file, file_list)
|
||||||
else:
|
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
|
||||||
|
});
|
||||||
119
.github/workflows/check_properties.yml
vendored
119
.github/workflows/check_properties.yml
vendored
@@ -6,18 +6,22 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "src/main/resources/messages_*.properties"
|
- "src/main/resources/messages_*.properties"
|
||||||
push:
|
push:
|
||||||
|
branches: ["main"]
|
||||||
paths:
|
paths:
|
||||||
- "src/main/resources/messages_en_GB.properties"
|
- "src/main/resources/messages_en_GB.properties"
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-files:
|
check-files:
|
||||||
if: github.event_name == 'pull_request_target'
|
if: github.event_name == 'pull_request_target'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout main branch first
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
path: main-branch
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout PR branch
|
- name: Checkout PR branch
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -26,13 +30,6 @@ jobs:
|
|||||||
path: pr-branch
|
path: pr-branch
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Checkout main branch
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
path: main-branch
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
@@ -49,56 +46,73 @@ jobs:
|
|||||||
echo "Fetching PR changed files..."
|
echo "Fetching PR changed files..."
|
||||||
cd pr-branch
|
cd pr-branch
|
||||||
gh repo set-default ${{ github.repository }}
|
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 ..
|
cd ..
|
||||||
echo $(cat changed_files.txt)
|
|
||||||
BRANCH_PATH="pr-branch"
|
echo "Processing changed files..."
|
||||||
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
|
mapfile -t CHANGED_FILES < changed_files.txt
|
||||||
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
|
|
||||||
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
|
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
|
||||||
echo "Branch: ${BRANCH_PATH}"
|
|
||||||
|
echo "Changed files: ${CHANGED_FILES_STR}"
|
||||||
|
|
||||||
- name: Determine reference file
|
- name: Determine reference file
|
||||||
id: determine-file
|
id: determine-file
|
||||||
run: |
|
run: |
|
||||||
echo "Determining reference file..."
|
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
|
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||||
else
|
else
|
||||||
|
echo "Using main branch reference file"
|
||||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
|
|
||||||
|
|
||||||
- name: Show 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
|
- name: Run Python script to check files
|
||||||
id: run-check
|
id: run-check
|
||||||
run: |
|
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
|
- name: Capture output
|
||||||
id: capture-output
|
id: capture-output
|
||||||
run: |
|
run: |
|
||||||
if [ -f failure.txt ] && [ -s failure.txt ]; then
|
if [ -f result.txt ] && [ -s result.txt ]; then
|
||||||
echo "Test failed, capturing output..."
|
echo "Test, capturing output..."
|
||||||
ERROR_OUTPUT=$(cat failure.txt)
|
SCRIPT_OUTPUT=$(cat result.txt)
|
||||||
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
|
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||||
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
|
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
|
||||||
echo "EOF" >> $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
|
else
|
||||||
echo "No errors found."
|
echo "No update found."
|
||||||
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
|
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
|
||||||
|
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Post comment on PR
|
- name: Post comment on PR
|
||||||
if: env.ERROR_OUTPUT != ''
|
if: env.SCRIPT_OUTPUT != ''
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
|
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||||
const prNumber = context.issue.number;
|
const prNumber = context.issue.number;
|
||||||
|
|
||||||
@@ -120,7 +134,7 @@ jobs:
|
|||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
comment_id: comment.id,
|
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.");
|
console.log("Updated existing comment.");
|
||||||
} else if (!comment) {
|
} else if (!comment) {
|
||||||
@@ -129,33 +143,24 @@ jobs:
|
|||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
issue_number: prNumber,
|
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.");
|
console.log("Created new comment.");
|
||||||
} else {
|
} else {
|
||||||
console.log("Comment update attempt denied. Actor does not match.");
|
console.log("Comment update attempt denied. Actor does not match.");
|
||||||
}
|
}
|
||||||
|
|
||||||
# - name: Set up git config
|
- name: Fail job if errors found
|
||||||
# run: |
|
if: env.FAIL_JOB == 'true'
|
||||||
# git config --global user.name "github-actions[bot]"
|
run: |
|
||||||
# git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
echo "Failing the job because errors were detected."
|
||||||
|
exit 1
|
||||||
# - 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"
|
|
||||||
|
|
||||||
update-translations-main:
|
update-translations-main:
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -169,7 +174,10 @@ jobs:
|
|||||||
- name: Run Python script to check files
|
- name: Run Python script to check files
|
||||||
id: run-check
|
id: run-check
|
||||||
run: |
|
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
|
- name: Set up git config
|
||||||
run: |
|
run: |
|
||||||
@@ -184,7 +192,7 @@ jobs:
|
|||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
if: env.CHANGES_DETECTED == 'true'
|
if: env.CHANGES_DETECTED == 'true'
|
||||||
uses: peter-evans/create-pull-request@v6
|
uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "Update translation files"
|
commit-message: "Update translation files"
|
||||||
@@ -193,6 +201,8 @@ jobs:
|
|||||||
signoff: true
|
signoff: true
|
||||||
branch: update_translation_files
|
branch: update_translation_files
|
||||||
title: "Update translation files"
|
title: "Update translation files"
|
||||||
|
add-paths: |
|
||||||
|
src/main/resources/messages_*.properties
|
||||||
body: |
|
body: |
|
||||||
Auto-generated by [create-pull-request][1]
|
Auto-generated by [create-pull-request][1]
|
||||||
|
|
||||||
@@ -200,3 +210,4 @@ jobs:
|
|||||||
labels: Translation
|
labels: Translation
|
||||||
draft: false
|
draft: false
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
|
sign-commits: true
|
||||||
|
|||||||
91
.github/workflows/multiOSReleases.yml
vendored
Normal file
91
.github/workflows/multiOSReleases.yml
vendored
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
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-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ 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-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}
|
||||||
|
retention-days: 1
|
||||||
|
if-no-files-found: error
|
||||||
3
.github/workflows/push-docker.yml
vendored
3
.github/workflows/push-docker.yml
vendored
@@ -67,6 +67,7 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/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
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
@@ -95,6 +96,7 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/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
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
@@ -122,6 +124,7 @@ jobs:
|
|||||||
images: |
|
images: |
|
||||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/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
|
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||||
|
|||||||
59
.github/workflows/releaseArtifacts.yml
vendored
59
.github/workflows/releaseArtifacts.yml
vendored
@@ -35,36 +35,37 @@ jobs:
|
|||||||
run: ./gradlew clean createExe
|
run: ./gradlew clean createExe
|
||||||
env:
|
env:
|
||||||
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
|
||||||
|
STIRLING_PDF_DESKTOP_UI: false
|
||||||
|
|
||||||
- name: Get version number
|
- name: Get version number
|
||||||
id: versionNumber
|
id: versionNumber
|
||||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Rename binarie
|
- name: Rename binarie
|
||||||
if: matrix.file_suffix != ''
|
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
|
||||||
|
|
||||||
- name: Upload Assets binarie
|
- name: Upload Assets binarie
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
name: Stirling-PDF${{ matrix.file_suffix }}.exe
|
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
overwrite: true
|
overwrite: true
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload binaries to release
|
- name: Upload binaries to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
|
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
|
||||||
|
|
||||||
- name: Rename jar binaries
|
- name: Rename jar binaries
|
||||||
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
- name: Upload Assets jar binaries
|
- name: Upload Assets jar binaries
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
path: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar
|
||||||
name: Stirling-PDF${{ matrix.file_suffix }}.jar
|
name: Stirling-PDF-Server${{ matrix.file_suffix }}.jar
|
||||||
overwrite: true
|
overwrite: true
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
@@ -72,4 +73,44 @@ jobs:
|
|||||||
- name: Upload jar binaries to release
|
- name: Upload jar binaries to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
|
files: ./build/libs/Stirling-PDF-Server${{ matrix.file_suffix }}.jar
|
||||||
|
|
||||||
|
|
||||||
|
push-ui:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: "17"
|
||||||
|
distribution: "temurin"
|
||||||
|
|
||||||
|
- uses: gradle/actions/setup-gradle@v4
|
||||||
|
with:
|
||||||
|
gradle-version: 8.7
|
||||||
|
|
||||||
|
- name: Generate exe
|
||||||
|
run: ./gradlew clean createExe
|
||||||
|
env:
|
||||||
|
DOCKER_ENABLE_SECURITY: false
|
||||||
|
STIRLING_PDF_DESKTOP_UI: true
|
||||||
|
|
||||||
|
- name: Get version number
|
||||||
|
id: versionNumber
|
||||||
|
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload Assets binarie
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ./build/launch4j/Stirling-PDF.exe
|
||||||
|
name: Stirling-PDF.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.exe
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -161,3 +161,4 @@ out/
|
|||||||
.pytest_cache
|
.pytest_cache
|
||||||
.ipynb_checkpoints
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
**/jcef-bundle/
|
||||||
@@ -11,7 +11,7 @@ Stirling-PDF is built using:
|
|||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- PDFBox
|
- PDFBox
|
||||||
- LibreOffice
|
- LibreOffice
|
||||||
- OcrMyPdf
|
- qpdf
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- Docker
|
- Docker
|
||||||
- PDF.js
|
- PDF.js
|
||||||
@@ -114,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Security
|
container_name: Stirling-PDF-Security
|
||||||
image: frooodle/s-pdf:latest
|
image: stirlingtools/stirling-pdf:latest
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
@@ -173,20 +173,20 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
|||||||
For the latest version:
|
For the latest version:
|
||||||
|
|
||||||
```bash
|
```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:
|
For the ultra-lite version:
|
||||||
|
|
||||||
```bash
|
```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):
|
For the fat version (with security enabled):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export DOCKER_ENABLE_SECURITY=true
|
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
|
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:
|
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!)
|
- 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.
|
- 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 \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
curl \
|
curl \
|
||||||
|
qpdf \
|
||||||
shadow \
|
shadow \
|
||||||
su-exec \
|
su-exec \
|
||||||
openssl \
|
openssl \
|
||||||
@@ -40,7 +41,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||||
ocrmypdf \
|
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
# CV
|
# CV
|
||||||
py3-opencv \
|
py3-opencv \
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Build the application
|
# Build the application
|
||||||
FROM gradle:8.7-jdk17 AS build
|
FROM gradle:8.11-jdk17 AS build
|
||||||
|
|
||||||
# Set the working directory
|
# Set the working directory
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -55,7 +55,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
|||||||
# pdftohtml
|
# pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||||
ocrmypdf \
|
qpdf \
|
||||||
tesseract-ocr-data-eng \
|
tesseract-ocr-data-eng \
|
||||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||||
# CV
|
# CV
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# use alpine
|
# use alpine
|
||||||
FROM alpine:3.20.3
|
FROM alpine:3.21.0
|
||||||
|
|
||||||
ARG VERSION_TAG
|
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 | ✔️ | | | | | | | | | | ✔️ | | |
|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
|
||||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
|
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
|
||||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||||
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
|
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
|
||||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | ✔️ |
|
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
|
||||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
|
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
|
||||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
|
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
|
||||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
|
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
|
||||||
| compare | | | | ✔️ | | | | | | | ✔️ | | |
|
| compare | | | | ✔️ | | | | | | | ✔️ | | |
|
||||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | ✔️ |
|
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
||||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||||
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
|
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
|
||||||
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
|
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
|
||||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
|
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
|
||||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
|
||||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | ✔️ |
|
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
|
||||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
|
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
|
||||||
| sign | | | | ✔️ | | | | | | | ✔️ | | |
|
| sign | | | | ✔️ | | | | | | | ✔️ | | |
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The paths have changed for the tessdata locations on new Docker images. Please u
|
|||||||
|
|
||||||
## How does the OCR Work
|
## How does the OCR Work
|
||||||
|
|
||||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF), which in turn uses Tesseract for its text recognition. 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
|
## Language Packs
|
||||||
|
|
||||||
@@ -52,8 +52,6 @@ Add the following to your existing Docker run command:
|
|||||||
|
|
||||||
### Non-Docker Setup
|
### 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:
|
For Debian-based systems, install languages with this command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -83,8 +81,7 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
|||||||
|
|
||||||
For Windows:
|
For Windows:
|
||||||
|
|
||||||
Ensure ocrmypdf in installed with
|
You must ensure tesseract is installed
|
||||||
``pip install ocrmypdf``
|
|
||||||
|
|
||||||
Additional languages must be downloaded manually:
|
Additional languages must be downloaded manually:
|
||||||
Download desired .traineddata files from tessdata or tessdata_fast
|
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
|
### 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:
|
Install the following software:
|
||||||
|
|
||||||
@@ -81,27 +81,27 @@ Install the following software:
|
|||||||
- unoconv
|
- unoconv
|
||||||
- pngquant
|
- pngquant
|
||||||
- unpaper
|
- unpaper
|
||||||
- ocrmypdf
|
- qpdf
|
||||||
- opencv-python-headless
|
- opencv-python-headless
|
||||||
|
|
||||||
For Debian-based systems, you can use the following command:
|
For Debian-based systems, you can use the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
||||||
```
|
```
|
||||||
|
|
||||||
For Fedora:
|
For Fedora:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
||||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||||
```
|
```
|
||||||
|
|
||||||
For Nix:
|
For Nix:
|
||||||
|
|
||||||
```bash
|
```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
|
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.
|
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`
|
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.
|
**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>
|
<h1 align="center">Stirling-PDF</h1>
|
||||||
|
|
||||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||||
[](https://discord.gg/Cn8pWhQRxZ)
|
[](https://discord.gg/HYmhKj45pU)
|
||||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||||
|
|
||||||
|
<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)
|
[](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.
|
[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
|
## 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
|
- Dark mode support
|
||||||
- Custom download options
|
- Custom download options
|
||||||
- Parallel file processing and downloads
|
- 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
|
- Detect and remove blank pages
|
||||||
- Compare two PDFs and show differences in text
|
- Compare two PDFs and show differences in text
|
||||||
- Add images to PDFs
|
- Add images to PDFs
|
||||||
- Compress PDFs to decrease their filesize (using OCRMyPDF)
|
- Compress PDFs to decrease their filesize (using qpdf)
|
||||||
- Extract images from PDF
|
- Extract images from PDF
|
||||||
- Remove images from PDF
|
- Remove images from PDF
|
||||||
- Extract images from scans
|
- Extract images from scans
|
||||||
- Remove annotations
|
- Remove annotations
|
||||||
- Add page numbers
|
- Add page numbers
|
||||||
- Auto rename file by detecting PDF header text
|
- Auto rename file by detecting PDF header text
|
||||||
- OCR on PDF (using OCRMyPDF)
|
- OCR on PDF (using tesseract)
|
||||||
- PDF/A conversion (using OCRMyPDF)
|
- PDF/A conversion (using libreoffice)
|
||||||
- Edit metadata
|
- Edit metadata
|
||||||
- Flatten PDFs
|
- Flatten PDFs
|
||||||
- Get all information on a PDF to view or export as JSON
|
- Get all information on a PDF to view or export as JSON
|
||||||
@@ -101,7 +102,7 @@ A demo of the app is available [here](https://stirlingpdf.io).
|
|||||||
- Spring Boot + Thymeleaf
|
- Spring Boot + Thymeleaf
|
||||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
- [qpdf](https://github.com/qpdf/qpdf)
|
||||||
- HTML, CSS, JavaScript
|
- HTML, CSS, JavaScript
|
||||||
- Docker
|
- Docker
|
||||||
- [PDF.js](https://github.com/mozilla/pdf.js)
|
- [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
|
### Docker / Podman
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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.
|
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`.
|
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 INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||||
-e LANGS=en_GB \
|
-e LANGS=en_GB \
|
||||||
--name stirling-pdf \
|
--name stirling-pdf \
|
||||||
frooodle/s-pdf:latest
|
stirlingtools/stirling-pdf:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
@@ -153,7 +154,7 @@ docker run -d \
|
|||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
image: frooodle/s-pdf:latest
|
image: stirlingtools/stirling-pdf:latest
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
@@ -186,46 +187,48 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
|
|||||||
|
|
||||||
## Supported Languages
|
## Supported Languages
|
||||||
|
|
||||||
Stirling-PDF currently supports 36 languages!
|
Stirling-PDF currently supports 38 languages!
|
||||||
|
|
||||||
| Language | Progress |
|
| Language | Progress |
|
||||||
| -------------------------------------------- | -------------------------------------- |
|
| -------------------------------------------- | -------------------------------------- |
|
||||||
| Arabic (العربية) (ar_AR) |  |
|
| Arabic (العربية) (ar_AR) |  |
|
||||||
| Basque (Euskara) (eu_ES) |  |
|
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||||
| Bulgarian (Български) (bg_BG) |  |
|
| Basque (Euskara) (eu_ES) |  |
|
||||||
| Catalan (Català) (ca_CA) |  |
|
| Bulgarian (Български) (bg_BG) |  |
|
||||||
| Croatian (Hrvatski) (hr_HR) |  |
|
| Catalan (Català) (ca_CA) |  |
|
||||||
| Czech (Česky) (cs_CZ) |  |
|
| Croatian (Hrvatski) (hr_HR) |  |
|
||||||
| Danish (Dansk) (da_DK) |  |
|
| Czech (Česky) (cs_CZ) |  |
|
||||||
| Dutch (Nederlands) (nl_NL) |  |
|
| Danish (Dansk) (da_DK) |  |
|
||||||
|
| Dutch (Nederlands) (nl_NL) |  |
|
||||||
| English (English) (en_GB) |  |
|
| English (English) (en_GB) |  |
|
||||||
| English (US) (en_US) |  |
|
| English (US) (en_US) |  |
|
||||||
| French (Français) (fr_FR) |  |
|
| French (Français) (fr_FR) |  |
|
||||||
| German (Deutsch) (de_DE) |  |
|
| German (Deutsch) (de_DE) |  |
|
||||||
| Greek (Ελληνικά) (el_GR) |  |
|
| Greek (Ελληνικά) (el_GR) |  |
|
||||||
| Hindi (हिंदी) (hi_IN) |  |
|
| Hindi (हिंदी) (hi_IN) |  |
|
||||||
| Hungarian (Magyar) (hu_HU) |  |
|
| Hungarian (Magyar) (hu_HU) |  |
|
||||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||||
| Irish (Gaeilge) (ga_IE) |  |
|
| Irish (Gaeilge) (ga_IE) |  |
|
||||||
| Italian (Italiano) (it_IT) |  |
|
| Italian (Italiano) (it_IT) |  |
|
||||||
| Japanese (日本語) (ja_JP) |  |
|
| Japanese (日本語) (ja_JP) |  |
|
||||||
| Korean (한국어) (ko_KR) |  |
|
| Korean (한국어) (ko_KR) |  |
|
||||||
| Norwegian (Norsk) (no_NB) |  |
|
| Norwegian (Norsk) (no_NB) |  |
|
||||||
| Polish (Polski) (pl_PL) |  |
|
| Persian (فارسی) (fa_IR) |  |
|
||||||
| Portuguese (Português) (pt_PT) |  |
|
| Polish (Polski) (pl_PL) |  |
|
||||||
|
| Portuguese (Português) (pt_PT) |  |
|
||||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||||
| Romanian (Română) (ro_RO) |  |
|
| Romanian (Română) (ro_RO) |  |
|
||||||
| Russian (Русский) (ru_RU) |  |
|
| Russian (Русский) (ru_RU) |  |
|
||||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||||
| Slovakian (Slovensky) (sk_SK) |  |
|
| Slovakian (Slovensky) (sk_SK) |  |
|
||||||
| Spanish (Español) (es_ES) |  |
|
| Spanish (Español) (es_ES) |  |
|
||||||
| Swedish (Svenska) (sv_SE) |  |
|
| Swedish (Svenska) (sv_SE) |  |
|
||||||
| Thai (ไทย) (th_TH) |  |
|
| Thai (ไทย) (th_TH) |  |
|
||||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||||
| Turkish (Türkçe) (tr_TR) |  |
|
| Turkish (Türkçe) (tr_TR) |  |
|
||||||
| Ukrainian (Українська) (uk_UA) |  |
|
| Ukrainian (Українська) (uk_UA) |  |
|
||||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||||
|
|
||||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
## 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
|
### 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 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
|
- Base SSO support
|
||||||
- Advanced SSO such as automated login handling (Coming very soon)
|
- Advanced SSO such as automated login handling (Coming very soon)
|
||||||
- SAML SSO (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.
|
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
|
## FAQ
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ The 'Fat' container contains all those found in 'Full' with security jar along w
|
|||||||
| Libre | | ✔️ |
|
| Libre | | ✔️ |
|
||||||
| Python | | ✔️ |
|
| Python | | ✔️ |
|
||||||
| OpenCV | | ✔️ |
|
| OpenCV | | ✔️ |
|
||||||
| OCRmyPDF | | ✔️ |
|
| qpdf | | ✔️ |
|
||||||
|
|
||||||
| Operation | Ultra-Lite | Full |
|
| Operation | Ultra-Lite | Full |
|
||||||
| ---------------------- | ---------- | ---- |
|
| ---------------------- | ---------- | ---- |
|
||||||
|
|||||||
217
build.gradle
217
build.gradle
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id "java"
|
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 "io.spring.dependency-management" version "1.1.6"
|
||||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||||
id "io.swagger.swaggerhub" version "1.3.2"
|
id "io.swagger.swaggerhub" version "1.3.2"
|
||||||
@@ -8,21 +8,27 @@ plugins {
|
|||||||
id "com.diffplug.spotless" version "6.25.0"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
//id "nebula.lint" version "19.0.3"
|
//id "nebula.lint" version "19.0.3"
|
||||||
|
id("org.panteleyev.jpackageplugin") version "1.6.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
springBootVersion = "3.3.5"
|
springBootVersion = "3.4.0"
|
||||||
pdfboxVersion = "3.0.3"
|
pdfboxVersion = "3.0.3"
|
||||||
logbackVersion = "1.5.7"
|
logbackVersion = "1.5.7"
|
||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.12.0"
|
||||||
lombokVersion = "1.18.34"
|
lombokVersion = "1.18.36"
|
||||||
bouncycastleVersion = "1.78.1"
|
bouncycastleVersion = "1.79"
|
||||||
|
springSecuritySamlVersion = "6.4.1"
|
||||||
|
openSamlVersion = "4.3.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "stirling.software"
|
group = "stirling.software"
|
||||||
version = "0.32.0"
|
version = "0.36.1"
|
||||||
|
|
||||||
|
|
||||||
java {
|
java {
|
||||||
// 17 is lowest but we support and recommend 21
|
// 17 is lowest but we support and recommend 21
|
||||||
@@ -36,6 +42,9 @@ repositories {
|
|||||||
maven {
|
maven {
|
||||||
url 'https://build.shibboleth.net/maven/releases'
|
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 {
|
licenseReport {
|
||||||
@@ -59,6 +68,12 @@ sourceSets {
|
|||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
exclude "stirling/software/SPDF/repository/**"
|
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"
|
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 {
|
launch4j {
|
||||||
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
icon = "${projectDir}/src/main/resources/static/favicon.ico"
|
||||||
|
|
||||||
outfile="Stirling-PDF.exe"
|
outfile="Stirling-PDF.exe"
|
||||||
headerType="console"
|
|
||||||
|
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
|
||||||
|
headerType = "gui"
|
||||||
|
} else {
|
||||||
|
headerType = "console"
|
||||||
|
}
|
||||||
jarTask = tasks.bootJar
|
jarTask = tasks.bootJar
|
||||||
|
|
||||||
errTitle="Encountered error, Do you have Java 21?"
|
errTitle="Encountered error, Do you have Java 21?"
|
||||||
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
|
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"
|
jreMinVersion="17"
|
||||||
|
|
||||||
mutexName="Stirling-PDF"
|
mutexName="Stirling-PDF"
|
||||||
@@ -118,8 +270,15 @@ configurations.all {
|
|||||||
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
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
|
//security updates
|
||||||
implementation "org.springframework:spring-webmvc:6.1.14"
|
implementation "org.springframework:spring-webmvc:6.2.0"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||||
|
|
||||||
@@ -137,22 +296,22 @@ dependencies {
|
|||||||
|
|
||||||
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
|
||||||
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
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-data-jpa:$springBootVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||||
|
|
||||||
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
|
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
||||||
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
//2.2.x requires rebuild of DB file.. need migration path
|
// Don't upgrade h2database
|
||||||
runtimeOnly "com.h2database:h2:2.1.214"
|
runtimeOnly "com.h2database:h2:2.3.232"
|
||||||
// implementation "com.h2database:h2:2.2.224"
|
|
||||||
constraints {
|
constraints {
|
||||||
implementation "org.opensaml:opensaml-core"
|
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||||
implementation "org.opensaml:opensaml-saml-api"
|
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||||
implementation "org.opensaml:opensaml-saml-impl"
|
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
|
||||||
}
|
}
|
||||||
implementation "org.springframework.security:spring-security-saml2-service-provider"
|
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'
|
implementation 'com.coveo:saml-client:5.0.0'
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +343,7 @@ dependencies {
|
|||||||
// Image metadata extractor
|
// Image metadata extractor
|
||||||
implementation "com.drewnoakes:metadata-extractor:2.19.0"
|
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"
|
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||||
//general PDF
|
//general PDF
|
||||||
|
|
||||||
@@ -201,12 +360,19 @@ dependencies {
|
|||||||
exclude group: "commons-logging", module: "commons-logging"
|
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.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||||
|
|
||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
implementation "io.micrometer:micrometer-core:1.13.6"
|
implementation "io.micrometer:micrometer-core:1.14.1"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.24.0"
|
implementation "org.commonmark:commonmark:0.24.0"
|
||||||
@@ -259,7 +425,14 @@ jar {
|
|||||||
tasks.named("test") {
|
tasks.named("test") {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
task printVersion {
|
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
|
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
|
@ocr @positive
|
||||||
Scenario: Process PDF with OCR
|
Scenario: Process PDF with OCR
|
||||||
Given I generate a PDF file as "fileInput"
|
Given I generate a PDF file as "fileInput"
|
||||||
@@ -83,26 +65,6 @@ Feature: API Validation
|
|||||||
Then the response content type should be "application/pdf"
|
Then the response content type should be "application/pdf"
|
||||||
And the response file should have size greater than 0
|
And the response file should have size greater than 0
|
||||||
And the response status code should be 200
|
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
|
@libre @positive
|
||||||
@@ -145,7 +107,7 @@ Feature: API Validation
|
|||||||
And the response file should have extension ".pdf"
|
And the response file should have extension ".pdf"
|
||||||
And the response file should have size greater than 100
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @qpdf @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
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 extension ".pdf"
|
||||||
And the response file should have size greater than 100
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @qpdf @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
@@ -169,7 +131,7 @@ Feature: API Validation
|
|||||||
And the response file should have size greater than 100
|
And the response file should have size greater than 100
|
||||||
|
|
||||||
|
|
||||||
@compress @ghostscript @positive
|
@compress @qpdf @positive
|
||||||
Scenario: Compress
|
Scenario: Compress
|
||||||
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
||||||
And the request data includes
|
And the request data includes
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ import shutil
|
|||||||
import re
|
import re
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
API_HEADERS = {
|
||||||
|
'X-API-KEY': '123456789'
|
||||||
|
}
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# GIVEN #
|
# GIVEN #
|
||||||
#########
|
#########
|
||||||
@@ -227,7 +231,7 @@ def save_generated_pdf(context, filename):
|
|||||||
def step_send_get_request(context, endpoint):
|
def step_send_get_request(context, endpoint):
|
||||||
base_url = "http://localhost:8080"
|
base_url = "http://localhost:8080"
|
||||||
full_url = f"{base_url}{endpoint}"
|
full_url = f"{base_url}{endpoint}"
|
||||||
response = requests.get(full_url)
|
response = requests.get(full_url, headers=API_HEADERS)
|
||||||
context.response = response
|
context.response = response
|
||||||
|
|
||||||
@when('I send a GET request to "{endpoint}" with parameters')
|
@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"
|
base_url = "http://localhost:8080"
|
||||||
params = {row['parameter']: row['value'] for row in context.table}
|
params = {row['parameter']: row['value'] for row in context.table}
|
||||||
full_url = f"{base_url}{endpoint}"
|
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
|
context.response = response
|
||||||
|
|
||||||
@when('I send the API request to the endpoint "{endpoint}"')
|
@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}")
|
print(f"form_data {file.name} with {mime_type}")
|
||||||
form_data.append((key, (file.name, file, 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
|
context.response = response
|
||||||
|
|
||||||
########
|
########
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Security-Fat
|
container_name: Stirling-PDF-Security-Fat
|
||||||
image: frooodle/s-pdf:latest-fat
|
image: stirlingtools/stirling-pdf:latest-fat
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Security
|
container_name: Stirling-PDF-Security
|
||||||
image: frooodle/s-pdf:latest
|
image: stirlingtools/stirling-pdf:latest
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Security
|
container_name: Stirling-PDF-Security
|
||||||
image: frooodle/s-pdf:latest
|
image: stirlingtools/stirling-pdf:latest
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Ultra-Lite-Security
|
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||||
image: frooodle/s-pdf:latest-ultra-lite
|
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF-Ultra-Lite
|
container_name: Stirling-PDF-Ultra-Lite
|
||||||
image: frooodle/s-pdf:latest-ultra-lite
|
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
services:
|
services:
|
||||||
stirling-pdf:
|
stirling-pdf:
|
||||||
container_name: Stirling-PDF
|
container_name: Stirling-PDF
|
||||||
image: frooodle/s-pdf:latest
|
image: stirlingtools/stirling-pdf:latest
|
||||||
deploy:
|
deploy:
|
||||||
resources:
|
resources:
|
||||||
limits:
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[az_AZ]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[bg_BG]
|
[bg_BG]
|
||||||
ignore = [
|
ignore = [
|
||||||
'language.direction',
|
'language.direction',
|
||||||
@@ -32,21 +37,25 @@ ignore = [
|
|||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
'AddStampRequest.position',
|
'AddStampRequest.position',
|
||||||
'home.pipeline.title'
|
|
||||||
'PDFToBook.selectText.1',
|
'PDFToBook.selectText.1',
|
||||||
'PDFToText.tags',
|
'PDFToText.tags',
|
||||||
'addPageNumbers.selectText.3',
|
'addPageNumbers.selectText.3',
|
||||||
'alphabet',
|
'alphabet',
|
||||||
'certSign.name',
|
'certSign.name',
|
||||||
|
'fileChooser.dragAndDrop',
|
||||||
|
'home.pipeline.title',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
|
'legal.impressum',
|
||||||
'licenses.version',
|
'licenses.version',
|
||||||
'pipeline.title',
|
'pipeline.title',
|
||||||
'pipelineOptions.pipelineHeader',
|
'pipelineOptions.pipelineHeader',
|
||||||
'pro',
|
'pro',
|
||||||
'sponsor',
|
'sponsor',
|
||||||
'text',
|
'text',
|
||||||
|
'validateSignature.cert.bits',
|
||||||
|
'validateSignature.cert.version',
|
||||||
|
'validateSignature.status',
|
||||||
'watermark.type.1',
|
'watermark.type.1',
|
||||||
'certSign.name',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[el_GR]
|
[el_GR]
|
||||||
@@ -57,7 +66,6 @@ ignore = [
|
|||||||
[es_ES]
|
[es_ES]
|
||||||
ignore = [
|
ignore = [
|
||||||
'adminUserSettings.roles',
|
'adminUserSettings.roles',
|
||||||
'color',
|
|
||||||
'error',
|
'error',
|
||||||
'language.direction',
|
'language.direction',
|
||||||
'no',
|
'no',
|
||||||
@@ -69,6 +77,11 @@ ignore = [
|
|||||||
'language.direction',
|
'language.direction',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[fa_IR]
|
||||||
|
ignore = [
|
||||||
|
'language.direction',
|
||||||
|
]
|
||||||
|
|
||||||
[fr_FR]
|
[fr_FR]
|
||||||
ignore = [
|
ignore = [
|
||||||
'AddStampRequest.alphabet',
|
'AddStampRequest.alphabet',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
translation_key="pdfToPDFA.credit"
|
translation_key="pdfToPDFA.credit"
|
||||||
old_value="OCRmyPDF"
|
old_value="qpdf"
|
||||||
new_value="ghostscript"
|
new_value="liibreoffice"
|
||||||
|
|
||||||
for file in ../src/main/resources/messages_*.properties; do
|
for file in ../src/main/resources/messages_*.properties; do
|
||||||
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Lazy
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class EEAppConfig {
|
public class EEAppConfig {
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ public class LicenseKeyChecker {
|
|||||||
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
||||||
this.licenseService = licenseService;
|
this.licenseService = licenseService;
|
||||||
this.applicationProperties = applicationProperties;
|
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() {
|
public void checkLicensePeriodically() {
|
||||||
checkLicense();
|
checkLicense();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF;
|
package stirling.software.SPDF;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -8,6 +9,9 @@ import java.nio.file.Paths;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -19,13 +23,16 @@ import org.springframework.core.env.Environment;
|
|||||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import io.github.pixee.security.SystemCommand;
|
import io.github.pixee.security.SystemCommand;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
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.config.ConfigInitializer;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@Slf4j
|
||||||
public class SPdfApplication {
|
public class SPdfApplication {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
|
||||||
@@ -67,36 +74,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 {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
|
|
||||||
SpringApplication app = new SpringApplication(SPdfApplication.class);
|
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.setAdditionalProfiles("default");
|
||||||
app.addInitializers(new ConfigInitializer());
|
app.addInitializers(new ConfigInitializer());
|
||||||
Map<String, String> propertyFiles = new HashMap<>();
|
Map<String, String> propertyFiles = new HashMap<>();
|
||||||
@@ -120,14 +110,20 @@ public class SPdfApplication {
|
|||||||
} else {
|
} else {
|
||||||
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
|
||||||
}
|
}
|
||||||
|
Properties finalProps = new Properties();
|
||||||
|
|
||||||
if (!propertyFiles.isEmpty()) {
|
if (!propertyFiles.isEmpty()) {
|
||||||
app.setDefaultProperties(
|
finalProps.putAll(
|
||||||
Collections.singletonMap(
|
Collections.singletonMap(
|
||||||
"spring.config.additional-location",
|
"spring.config.additional-location",
|
||||||
propertyFiles.get("spring.config.additional-location")));
|
propertyFiles.get("spring.config.additional-location")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!props.isEmpty()) {
|
||||||
|
finalProps.putAll(props);
|
||||||
|
}
|
||||||
|
app.setDefaultProperties(finalProps);
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
|
|
||||||
// Ensure directories are created
|
// Ensure directories are created
|
||||||
@@ -147,6 +143,46 @@ public class SPdfApplication {
|
|||||||
logger.info("Navigate to {}", url);
|
logger.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) {
|
||||||
|
logger.error("Error opening browser: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info("Running configs {}", applicationProperties.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void cleanup() {
|
||||||
|
if (webBrowser != null) {
|
||||||
|
webBrowser.cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static String getStaticBaseUrl() {
|
public static String getStaticBaseUrl() {
|
||||||
return baseUrlStatic;
|
return baseUrlStatic;
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||||
|
|
||||||
// LibreOffice
|
// LibreOffice
|
||||||
addEndpointToGroup("LibreOffice", "repair");
|
addEndpointToGroup("qpdf", "repair");
|
||||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||||
@@ -199,10 +199,11 @@ public class EndpointConfiguration {
|
|||||||
// Unoconv
|
// Unoconv
|
||||||
addEndpointToGroup("Unoconv", "file-to-pdf");
|
addEndpointToGroup("Unoconv", "file-to-pdf");
|
||||||
|
|
||||||
// OCRmyPDF
|
// qpdf
|
||||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
addEndpointToGroup("qpdf", "compress-pdf");
|
||||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
|
||||||
|
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||||
|
|
||||||
// Java
|
// Java
|
||||||
addEndpointToGroup("Java", "merge-pdfs");
|
addEndpointToGroup("Java", "merge-pdfs");
|
||||||
@@ -248,10 +249,10 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Javascript", "compare");
|
addEndpointToGroup("Javascript", "compare");
|
||||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||||
|
|
||||||
// Ghostscript dependent endpoints
|
// qpdf dependent endpoints
|
||||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
addEndpointToGroup("qpdf", "compress-pdf");
|
||||||
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
|
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||||
addEndpointToGroup("Ghostscript", "repair");
|
addEndpointToGroup("qpdf", "repair");
|
||||||
|
|
||||||
// Weasyprint dependent endpoints
|
// Weasyprint dependent endpoints
|
||||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
||||||
@@ -259,6 +260,9 @@ public class EndpointConfiguration {
|
|||||||
|
|
||||||
// Pdftohtml dependent endpoints
|
// Pdftohtml dependent endpoints
|
||||||
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
addEndpointToGroup("Pdftohtml", "pdf-to-html");
|
||||||
|
|
||||||
|
// disabled for now while we resolve issues
|
||||||
|
disableEndpoint("pdf-to-pdfa");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processEnvironmentConfigs() {
|
private void processEnvironmentConfigs() {
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ public class ExternalAppDepConfig {
|
|||||||
private final Map<String, List<String>> commandToGroupMapping =
|
private final Map<String, List<String>> commandToGroupMapping =
|
||||||
new HashMap<>() {
|
new HashMap<>() {
|
||||||
{
|
{
|
||||||
put("gs", List.of("Ghostscript"));
|
|
||||||
put("soffice", List.of("LibreOffice"));
|
put("soffice", List.of("LibreOffice"));
|
||||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
|
||||||
put("weasyprint", List.of("Weasyprint"));
|
put("weasyprint", List.of("Weasyprint"));
|
||||||
put("pdftohtml", List.of("Pdftohtml"));
|
put("pdftohtml", List.of("Pdftohtml"));
|
||||||
put("unoconv", List.of("Unoconv"));
|
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() {
|
public void checkDependencies() {
|
||||||
|
|
||||||
// Check core dependencies
|
// Check core dependencies
|
||||||
checkDependencyAndDisableGroup("gs");
|
checkDependencyAndDisableGroup("tesseract");
|
||||||
checkDependencyAndDisableGroup("soffice");
|
checkDependencyAndDisableGroup("soffice");
|
||||||
checkDependencyAndDisableGroup("ocrmypdf");
|
checkDependencyAndDisableGroup("qpdf");
|
||||||
checkDependencyAndDisableGroup("weasyprint");
|
checkDependencyAndDisableGroup("weasyprint");
|
||||||
checkDependencyAndDisableGroup("pdftohtml");
|
checkDependencyAndDisableGroup("pdftohtml");
|
||||||
checkDependencyAndDisableGroup("unoconv");
|
checkDependencyAndDisableGroup("unoconv");
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package stirling.software.SPDF.config;
|
package stirling.software.SPDF.config;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import io.micrometer.common.util.StringUtils;
|
import io.micrometer.common.util.StringUtils;
|
||||||
@@ -23,6 +26,18 @@ public class InitialSetup {
|
|||||||
@Autowired private ApplicationProperties applicationProperties;
|
@Autowired private ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
|
public void init() throws IOException {
|
||||||
|
initUUIDKey();
|
||||||
|
|
||||||
|
initSecretKey();
|
||||||
|
|
||||||
|
initEnableCSRFSecurity();
|
||||||
|
|
||||||
|
initLegalUrls();
|
||||||
|
|
||||||
|
initSetAppVersion();
|
||||||
|
}
|
||||||
|
|
||||||
public void initUUIDKey() throws IOException {
|
public void initUUIDKey() throws IOException {
|
||||||
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
|
||||||
if (!GeneralUtils.isValidUUID(uuid)) {
|
if (!GeneralUtils.isValidUUID(uuid)) {
|
||||||
@@ -32,7 +47,6 @@ public class InitialSetup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostConstruct
|
|
||||||
public void initSecretKey() throws IOException {
|
public void initSecretKey() throws IOException {
|
||||||
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
|
||||||
if (!GeneralUtils.isValidUUID(secretKey)) {
|
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 {
|
public void initLegalUrls() throws IOException {
|
||||||
// Initialize Terms and Conditions
|
// Initialize Terms and Conditions
|
||||||
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
|
||||||
if (StringUtils.isEmpty(termsUrl)) {
|
if (StringUtils.isEmpty(termsUrl)) {
|
||||||
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
|
||||||
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
|
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false);
|
||||||
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +81,23 @@ public class InitialSetup {
|
|||||||
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
|
||||||
if (StringUtils.isEmpty(privacyUrl)) {
|
if (StringUtils.isEmpty(privacyUrl)) {
|
||||||
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
|
||||||
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
|
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false);
|
||||||
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class InitialSecuritySetup {
|
|||||||
initializeAdminUser();
|
initializeAdminUser();
|
||||||
} else {
|
} else {
|
||||||
databaseBackupHelper.exportDatabase();
|
databaseBackupHelper.exportDatabase();
|
||||||
|
userService.migrateOauth2ToSSO();
|
||||||
}
|
}
|
||||||
initializeInternalApiUser();
|
initializeInternalApiUser();
|
||||||
}
|
}
|
||||||
@@ -74,5 +75,6 @@ public class InitialSecuritySetup {
|
|||||||
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
|
||||||
log.info("Internal API user created: " + 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.security.cert.X509Certificate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.DependsOn;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.core.io.Resource;
|
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.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
@@ -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.InMemoryRelyingPartyRegistrationRepository;
|
||||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
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.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.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||||
@@ -41,6 +44,7 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
|||||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||||
@@ -64,6 +68,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
|||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
@EnableMethodSecurity
|
@EnableMethodSecurity
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@DependsOn("runningEE")
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
|
|
||||||
@Autowired private CustomUserDetailsService userDetailsService;
|
@Autowired private CustomUserDetailsService userDetailsService;
|
||||||
@@ -79,6 +84,10 @@ public class SecurityConfiguration {
|
|||||||
@Qualifier("loginEnabled")
|
@Qualifier("loginEnabled")
|
||||||
public boolean loginEnabledValue;
|
public boolean loginEnabledValue;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
@Qualifier("runningEE")
|
||||||
|
public boolean runningEE;
|
||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@Autowired ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||||
@@ -90,13 +99,14 @@ public class SecurityConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
|
||||||
|
http.csrf(csrf -> csrf.disable());
|
||||||
|
}
|
||||||
|
|
||||||
if (loginEnabledValue) {
|
if (loginEnabledValue) {
|
||||||
http.addFilterBefore(
|
http.addFilterBefore(
|
||||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||||
http.csrf(csrf -> csrf.disable());
|
|
||||||
} else {
|
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
CookieCsrfTokenRepository cookieRepo =
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
CsrfTokenRequestAttributeHandler requestHandler =
|
||||||
@@ -106,7 +116,7 @@ public class SecurityConfiguration {
|
|||||||
csrf ->
|
csrf ->
|
||||||
csrf.ignoringRequestMatchers(
|
csrf.ignoringRequestMatchers(
|
||||||
request -> {
|
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
|
// If there's no API key, don't ignore CSRF
|
||||||
// (return false)
|
// (return false)
|
||||||
@@ -245,12 +255,22 @@ public class SecurityConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle SAML
|
// Handle SAML
|
||||||
if (applicationProperties.getSecurity().isSaml2Activ()
|
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
|
||||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
// Configure the authentication provider
|
||||||
http.authenticationProvider(samlAuthenticationProvider());
|
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||||
http.saml2Login(
|
new OpenSaml4AuthenticationProvider();
|
||||||
saml2 ->
|
authenticationProvider.setResponseAuthenticationConverter(
|
||||||
|
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||||
|
|
||||||
|
http.authenticationProvider(authenticationProvider)
|
||||||
|
.saml2Login(
|
||||||
|
saml2 -> {
|
||||||
|
try {
|
||||||
saml2.loginPage("/saml2")
|
saml2.loginPage("/saml2")
|
||||||
|
.relyingPartyRegistrationRepository(
|
||||||
|
relyingPartyRegistrations())
|
||||||
|
.authenticationManager(
|
||||||
|
new ProviderManager(authenticationProvider))
|
||||||
.successHandler(
|
.successHandler(
|
||||||
new CustomSaml2AuthenticationSuccessHandler(
|
new CustomSaml2AuthenticationSuccessHandler(
|
||||||
loginAttemptService,
|
loginAttemptService,
|
||||||
@@ -258,44 +278,34 @@ public class SecurityConfiguration {
|
|||||||
userService))
|
userService))
|
||||||
.failureHandler(
|
.failureHandler(
|
||||||
new CustomSaml2AuthenticationFailureHandler())
|
new CustomSaml2AuthenticationFailureHandler())
|
||||||
.permitAll())
|
.authenticationRequestResolver(
|
||||||
.addFilterBefore(
|
authenticationRequestResolver(
|
||||||
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
|
relyingPartyRegistrations()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error configuring SAML2 login", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||||
http.csrf(csrf -> csrf.disable());
|
// CookieCsrfTokenRepository cookieRepo =
|
||||||
} else {
|
// CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||||
CookieCsrfTokenRepository cookieRepo =
|
// CsrfTokenRequestAttributeHandler requestHandler =
|
||||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
// new CsrfTokenRequestAttributeHandler();
|
||||||
CsrfTokenRequestAttributeHandler requestHandler =
|
// requestHandler.setCsrfRequestAttributeName(null);
|
||||||
new CsrfTokenRequestAttributeHandler();
|
// http.csrf(
|
||||||
requestHandler.setCsrfRequestAttributeName(null);
|
// csrf ->
|
||||||
http.csrf(
|
// csrf.csrfTokenRepository(cookieRepo)
|
||||||
csrf ->
|
// .csrfTokenRequestHandler(requestHandler));
|
||||||
csrf.csrfTokenRepository(cookieRepo)
|
// }
|
||||||
.csrfTokenRequestHandler(requestHandler));
|
|
||||||
}
|
|
||||||
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
|
||||||
}
|
}
|
||||||
|
|
||||||
return http.build();
|
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
|
@Bean
|
||||||
@ConditionalOnProperty(
|
@ConditionalOnProperty(
|
||||||
value = "security.oauth2.enabled",
|
value = "security.oauth2.enabled",
|
||||||
@@ -432,11 +442,12 @@ public class SecurityConfiguration {
|
|||||||
havingValue = "true",
|
havingValue = "true",
|
||||||
matchIfMissing = false)
|
matchIfMissing = false)
|
||||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||||
|
|
||||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
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();
|
Resource certificateResource = samlConf.getSpCert();
|
||||||
|
|
||||||
Saml2X509Credential signingCredential =
|
Saml2X509Credential signingCredential =
|
||||||
@@ -445,26 +456,97 @@ public class SecurityConfiguration {
|
|||||||
CertificateUtils.readCertificate(certificateResource),
|
CertificateUtils.readCertificate(certificateResource),
|
||||||
Saml2X509CredentialType.SIGNING);
|
Saml2X509CredentialType.SIGNING);
|
||||||
|
|
||||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
|
||||||
|
|
||||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
|
||||||
|
|
||||||
RelyingPartyRegistration rp =
|
RelyingPartyRegistration rp =
|
||||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||||
.signingX509Credentials((c) -> c.add(signingCredential))
|
.signingX509Credentials(c -> c.add(signingCredential))
|
||||||
.assertingPartyDetails(
|
.assertingPartyMetadata(
|
||||||
(details) ->
|
metadata ->
|
||||||
details.entityId(samlConf.getIdpIssuer())
|
metadata.entityId(samlConf.getIdpIssuer())
|
||||||
.singleSignOnServiceLocation(
|
.singleSignOnServiceLocation(
|
||||||
samlConf.getIdpSingleLoginUrl())
|
samlConf.getIdpSingleLoginUrl())
|
||||||
.verificationX509Credentials(
|
.verificationX509Credentials(
|
||||||
(c) -> c.add(verificationCredential))
|
c -> c.add(verificationCredential))
|
||||||
|
.singleSignOnServiceBinding(
|
||||||
|
Saml2MessageBinding.POST)
|
||||||
.wantAuthnRequestsSigned(true))
|
.wantAuthnRequestsSigned(true))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@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() {
|
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||||
provider.setUserDetailsService(userDetailsService);
|
provider.setUserDetailsService(userDetailsService);
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// Check for API key in the request headers if no authentication exists
|
// Check for API key in the request headers if no authentication exists
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
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()) {
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
try {
|
try {
|
||||||
// Use API key to authenticate. This requires you to have an authentication
|
// Use API key to authenticate. This requires you to have an authentication
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
String identifier = null;
|
String identifier = null;
|
||||||
|
|
||||||
// Check for API key in the request headers
|
// 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()) {
|
if (apiKey != null && !apiKey.trim().isEmpty()) {
|
||||||
identifier =
|
identifier =
|
||||||
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
|
||||||
@@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
|
|||||||
Role userRole =
|
Role userRole =
|
||||||
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
|
||||||
|
|
||||||
if (request.getHeader("X-API-Key") != null) {
|
if (request.getHeader("X-API-KEY") != null) {
|
||||||
// It's an API call
|
// It's an API call
|
||||||
processRequest(
|
processRequest(
|
||||||
userRole.getApiCallsPerDay(),
|
userRole.getApiCallsPerDay(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||||
@@ -50,8 +51,19 @@ public class UserService implements UserServiceInterface {
|
|||||||
|
|
||||||
@Autowired ApplicationProperties applicationProperties;
|
@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.
|
// Handle OAUTH2 login and user auto creation.
|
||||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||||
throws IllegalArgumentException, IOException {
|
throws IllegalArgumentException, IOException {
|
||||||
if (!isUsernameValid(username)) {
|
if (!isUsernameValid(username)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -61,7 +73,7 @@ public class UserService implements UserServiceInterface {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (autoCreateUser) {
|
if (autoCreateUser) {
|
||||||
saveUser(username, AuthenticationType.OAUTH2);
|
saveUser(username, AuthenticationType.SSO);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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
|
@Override
|
||||||
public long getTotalUsersCount() {
|
public long getTotalUsersCount() {
|
||||||
return userRepository.count();
|
return userRepository.count();
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
@Value("${spring.datasource.url}")
|
@Value("${spring.datasource.url}")
|
||||||
private String 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/");
|
private Path backupPath = Paths.get("configs/db/backup/");
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -134,7 +140,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
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)) {
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
stmt.setString(1, insertOutputFilePath.toString());
|
stmt.setString(1, insertOutputFilePath.toString());
|
||||||
stmt.execute();
|
stmt.execute();
|
||||||
@@ -147,7 +154,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
// Retrieves the H2 database version.
|
// Retrieves the H2 database version.
|
||||||
public String getH2Version() {
|
public String getH2Version() {
|
||||||
String version = "Unknown";
|
String version = "Unknown";
|
||||||
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
|
try (Connection conn =
|
||||||
|
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
||||||
try (Statement stmt = conn.createStatement();
|
try (Statement stmt = conn.createStatement();
|
||||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||||
if (rs.next()) {
|
if (rs.next()) {
|
||||||
@@ -189,7 +197,8 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
|||||||
private boolean executeDatabaseScript(Path scriptPath) {
|
private boolean executeDatabaseScript(Path scriptPath) {
|
||||||
String query = "RUNSCRIPT from ?;";
|
String query = "RUNSCRIPT from ?;";
|
||||||
|
|
||||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
try (Connection conn =
|
||||||
|
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||||
stmt.setString(1, scriptPath.toString());
|
stmt.setString(1, scriptPath.toString());
|
||||||
stmt.execute();
|
stmt.execute();
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
}
|
}
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
if (userService.usernameExistsIgnoreCase(username)
|
||||||
&& userService.hasPassword(username)
|
&& userService.hasPassword(username)
|
||||||
&& !userService.isAuthenticationTypeByUsername(
|
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||||
username, AuthenticationType.OAUTH2)
|
|
||||||
&& oAuth.getAutoCreateUser()) {
|
&& oAuth.getAutoCreateUser()) {
|
||||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
@@ -95,7 +94,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (principal instanceof OAuth2User) {
|
if (principal instanceof OAuth2User) {
|
||||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||||
}
|
}
|
||||||
response.sendRedirect(contextPath + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.interfaces.RSAPrivateKey;
|
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.PemObject;
|
||||||
import org.bouncycastle.util.io.pem.PemReader;
|
import org.bouncycastle.util.io.pem.PemReader;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
@@ -28,15 +30,26 @@ public class CertificateUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
||||||
try (PemReader pemReader =
|
try (PEMParser pemParser =
|
||||||
new PemReader(
|
new PEMParser(
|
||||||
new InputStreamReader(
|
new InputStreamReader(
|
||||||
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
||||||
PemObject pemObject = pemReader.readPemObject();
|
|
||||||
byte[] decodedKey = pemObject.getContent();
|
Object object = pemParser.readObject();
|
||||||
return (RSAPrivateKey)
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||||
KeyFactory.getInstance("RSA")
|
|
||||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
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.HttpServletResponse;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
import stirling.software.SPDF.config.security.UserService;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
@@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
|
|||||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class CustomSaml2AuthenticationSuccessHandler
|
public class CustomSaml2AuthenticationSuccessHandler
|
||||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||||
|
|
||||||
private LoginAttemptService loginAttemptService;
|
private LoginAttemptService loginAttemptService;
|
||||||
|
|
||||||
private ApplicationProperties applicationProperties;
|
private ApplicationProperties applicationProperties;
|
||||||
private UserService userService;
|
private UserService userService;
|
||||||
|
|
||||||
@@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
|
log.debug("Starting SAML2 authentication success handling");
|
||||||
|
|
||||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||||
// Get the saved request
|
log.debug("Authenticated principal found for user: {}", username);
|
||||||
|
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
String contextPath = request.getContextPath();
|
String contextPath = request.getContextPath();
|
||||||
SavedRequest savedRequest =
|
SavedRequest savedRequest =
|
||||||
@@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
|
|||||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
log.debug(
|
||||||
|
"Session exists: {}, Saved request exists: {}",
|
||||||
|
session != null,
|
||||||
|
savedRequest != null);
|
||||||
|
|
||||||
if (savedRequest != null
|
if (savedRequest != null
|
||||||
&& !RequestUriUtils.isStaticResource(
|
&& !RequestUriUtils.isStaticResource(
|
||||||
contextPath, savedRequest.getRedirectUrl())) {
|
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);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
} else {
|
} else {
|
||||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
||||||
|
log.debug(
|
||||||
|
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||||
|
saml2.getAutoCreateUser());
|
||||||
|
|
||||||
if (loginAttemptService.isBlocked(username)) {
|
if (loginAttemptService.isBlocked(username)) {
|
||||||
|
log.debug("User {} is blocked due to too many login attempts", username);
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||||
}
|
}
|
||||||
throw new LockedException(
|
throw new LockedException(
|
||||||
"Your account has been locked due to too many failed login attempts.");
|
"Your account has been locked due to too many failed login attempts.");
|
||||||
}
|
}
|
||||||
if (userService.usernameExistsIgnoreCase(username)
|
|
||||||
&& userService.hasPassword(username)
|
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||||
&& !userService.isAuthenticationTypeByUsername(
|
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||||
username, AuthenticationType.OAUTH2)
|
boolean isSSOUser =
|
||||||
&& saml2.getAutoCreateUser()) {
|
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(
|
response.sendRedirect(
|
||||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (saml2.getBlockRegistration()
|
if (saml2.getBlockRegistration() && !userExists) {
|
||||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
log.debug("Registration blocked for new user: {}", username);
|
||||||
response.sendRedirect(
|
response.sendRedirect(
|
||||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||||
return;
|
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 + "/");
|
response.sendRedirect(contextPath + "/");
|
||||||
return;
|
return;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.debug(
|
||||||
|
"Invalid username detected for user: {}, redirecting to logout",
|
||||||
|
username);
|
||||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log.debug("Non-SAML2 principal detected, delegating to parent handler");
|
||||||
super.onAuthenticationSuccess(request, response, authentication);
|
super.onAuthenticationSuccess(request, response, authentication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import org.opensaml.core.xml.XMLObject;
|
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.Assertion;
|
||||||
import org.opensaml.saml.saml2.core.Attribute;
|
import org.opensaml.saml.saml2.core.Attribute;
|
||||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||||
@@ -30,15 +28,60 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
this.userService = userService;
|
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
|
@Override
|
||||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||||
// Extract the assertion from the response
|
|
||||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
||||||
|
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||||
|
|
||||||
// Extract the NameID
|
// Debug log with actual values
|
||||||
String nameId = assertion.getSubject().getNameID().getValue();
|
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");
|
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||||
if (userOpt.isPresent()) {
|
if (userOpt.isPresent()) {
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
@@ -48,39 +91,27 @@ public class CustomSaml2ResponseAuthenticationConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the SessionIndexes
|
|
||||||
List<String> sessionIndexes = new ArrayList<>();
|
List<String> sessionIndexes = new ArrayList<>();
|
||||||
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
||||||
sessionIndexes.add(authnStatement.getSessionIndex());
|
sessionIndexes.add(authnStatement.getSessionIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the Attributes
|
|
||||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
|
||||||
|
|
||||||
// Create the custom principal
|
|
||||||
CustomSaml2AuthenticatedPrincipal principal =
|
CustomSaml2AuthenticatedPrincipal principal =
|
||||||
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
|
new CustomSaml2AuthenticatedPrincipal(
|
||||||
|
userIdentifier, attributes, userIdentifier, sessionIndexes);
|
||||||
|
|
||||||
// Create the Saml2Authentication
|
|
||||||
return new Saml2Authentication(
|
return new Saml2Authentication(
|
||||||
principal,
|
principal,
|
||||||
responseToken.getToken().getSaml2Response(),
|
responseToken.getToken().getSaml2Response(),
|
||||||
Collections.singletonList(simpleGrantedAuthority));
|
Collections.singletonList(simpleGrantedAuthority));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||||
Map<String, List<Object>> attributes = new HashMap<>();
|
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
|
||||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
}
|
||||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
|
||||||
String attributeName = attribute.getName();
|
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
|
||||||
List<Object> values = new ArrayList<>();
|
List<Object> values = attributes.get(name);
|
||||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
|
||||||
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
|
|
||||||
values.add(((XSString) xmlObject).getValue());
|
|
||||||
}
|
|
||||||
attributes.put(attributeName, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attributes;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,84 +52,115 @@ 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")
|
"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)
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile file = request.getFileInput();
|
|
||||||
String pages = request.getPageNumbers();
|
|
||||||
// open the pdf document
|
|
||||||
|
|
||||||
PDDocument document = Loader.loadPDF(file.getBytes());
|
PDDocument document = null;
|
||||||
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
|
Path zipFile = null;
|
||||||
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
|
|
||||||
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
|
||||||
int previousPageNumber = 0;
|
|
||||||
for (int splitPoint : pageNumbers) {
|
try {
|
||||||
try (PDDocument splitDocument =
|
|
||||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
|
MultipartFile file = request.getFileInput();
|
||||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
String pages = request.getPageNumbers();
|
||||||
PDPage page = document.getPage(i);
|
// open the pdf document
|
||||||
splitDocument.addPage(page);
|
|
||||||
logger.info("Adding page {} to split document", i);
|
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
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
logger.error("Failed splitting documents and saving them", e);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
previousPageNumber = splitPoint + 1;
|
}
|
||||||
|
|
||||||
// Transfer metadata to split pdf
|
// closing the original document
|
||||||
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
|
document.close();
|
||||||
|
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
zipFile = Files.createTempFile("split_documents", ".zip");
|
||||||
splitDocument.save(baos);
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
logger.info("Wrote split document {} to zip file", fileName);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed splitting documents and saving them", e);
|
logger.error("Failed writing to zip", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// closing the original document
|
logger.info(
|
||||||
document.close();
|
"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 =
|
} finally {
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
try {
|
||||||
.replaceFirst("[.][^.]+$", "");
|
// Close the main document
|
||||||
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
|
if (document != null) {
|
||||||
// loop through the split documents and write them to the zip file
|
document.close();
|
||||||
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
|
// Close all ByteArrayOutputStreams
|
||||||
ZipEntry pdfEntry = new ZipEntry(fileName);
|
for (ByteArrayOutputStream baos : splitDocumentsBoas) {
|
||||||
zipOut.putNextEntry(pdfEntry);
|
if (baos != null) {
|
||||||
zipOut.write(pdf);
|
baos.close();
|
||||||
zipOut.closeEntry();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Wrote split document {} to zip file", fileName);
|
// Delete temporary zip file
|
||||||
|
if (zipFile != null) {
|
||||||
|
Files.deleteIfExists(zipFile);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,70 +59,86 @@ public class SplitPdfByChaptersController {
|
|||||||
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
|
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile file = request.getFileInput();
|
MultipartFile file = request.getFileInput();
|
||||||
boolean includeMetadata = request.getIncludeMetadata();
|
PDDocument sourceDocument = null;
|
||||||
Integer bookmarkLevel =
|
Path zipFile = null;
|
||||||
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());
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
bookmarks =
|
boolean includeMetadata = request.getIncludeMetadata();
|
||||||
extractOutlineItems(
|
Integer bookmarkLevel =
|
||||||
sourceDocument,
|
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||||
outline.getFirstChild(),
|
if (bookmarkLevel < 0) {
|
||||||
bookmarks,
|
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
|
||||||
outline.getFirstChild().getNextSibling(),
|
}
|
||||||
0,
|
sourceDocument = Loader.loadPDF(file.getBytes());
|
||||||
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) {
|
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
||||||
logger.error("Unable to extract outline items", e);
|
|
||||||
return ResponseEntity.internalServerError()
|
if (outline == null) {
|
||||||
.body("Unable to extract outline items".getBytes());
|
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);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.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) {
|
||||||
|
logger.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) {
|
||||||
|
logger.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) {
|
private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) {
|
||||||
|
|||||||
@@ -105,15 +105,13 @@ public class SplitPdfBySectionsController {
|
|||||||
|
|
||||||
if (sectionNum == horiz * verti) pageNum++;
|
if (sectionNum == horiz * verti) pageNum++;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("exception", e);
|
|
||||||
} finally {
|
|
||||||
data = Files.readAllBytes(zipFile);
|
data = Files.readAllBytes(zipFile);
|
||||||
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
|
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
|
} finally {
|
||||||
Files.deleteIfExists(zipFile);
|
Files.deleteIfExists(zipFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
|
||||||
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<PDDocument> splitPdfPages(
|
public List<PDDocument> splitPdfPages(
|
||||||
|
|||||||
@@ -244,8 +244,8 @@ public class UserController {
|
|||||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||||
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
userService.saveUser(username, AuthenticationType.SSO, role);
|
||||||
} else {
|
} else {
|
||||||
if (password.isBlank()) {
|
if (password.isBlank()) {
|
||||||
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||||
|
|||||||
@@ -65,112 +65,137 @@ public class ConvertImgPDFController {
|
|||||||
String colorType = request.getColorType();
|
String colorType = request.getColorType();
|
||||||
String dpi = request.getDpi();
|
String dpi = request.getDpi();
|
||||||
|
|
||||||
byte[] pdfBytes = file.getBytes();
|
Path tempFile = null;
|
||||||
ImageType colorTypeResult = ImageType.RGB;
|
Path tempOutputDir = null;
|
||||||
if ("greyscale".equals(colorType)) {
|
Path tempPdfPath = null;
|
||||||
colorTypeResult = ImageType.GRAY;
|
|
||||||
} else if ("blackwhite".equals(colorType)) {
|
|
||||||
colorTypeResult = ImageType.BINARY;
|
|
||||||
}
|
|
||||||
// returns bytes for image
|
|
||||||
boolean singleImage = "single".equals(singleOrMultiple);
|
|
||||||
byte[] result = null;
|
byte[] result = null;
|
||||||
String filename =
|
|
||||||
Filenames.toSimpleFileName(file.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "");
|
|
||||||
|
|
||||||
result =
|
try {
|
||||||
PdfUtils.convertFromPdf(
|
byte[] pdfBytes = file.getBytes();
|
||||||
pdfBytes,
|
ImageType colorTypeResult = ImageType.RGB;
|
||||||
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
|
if ("greyscale".equals(colorType)) {
|
||||||
colorTypeResult,
|
colorTypeResult = ImageType.GRAY;
|
||||||
singleImage,
|
} else if ("blackwhite".equals(colorType)) {
|
||||||
Integer.valueOf(dpi),
|
colorTypeResult = ImageType.BINARY;
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
// returns bytes for image
|
||||||
|
boolean singleImage = "single".equals(singleOrMultiple);
|
||||||
|
String filename =
|
||||||
|
Filenames.toSimpleFileName(file.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "");
|
||||||
|
|
||||||
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
result =
|
||||||
|
PdfUtils.convertFromPdf(
|
||||||
List<String> command = new ArrayList<>();
|
pdfBytes,
|
||||||
command.add(pythonVersion);
|
"webp".equalsIgnoreCase(imageFormat)
|
||||||
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
? "png"
|
||||||
|
: imageFormat.toUpperCase(),
|
||||||
// Create a temporary directory for the output WebP files
|
colorTypeResult,
|
||||||
Path tempOutputDir = Files.createTempDirectory("webp_output");
|
singleImage,
|
||||||
if (singleImage) {
|
Integer.valueOf(dpi),
|
||||||
// Run the Python script to convert PNG to WebP
|
filename);
|
||||||
command.add(tempFile.toString());
|
if (result == null || result.length == 0) {
|
||||||
command.add(tempOutputDir.toString());
|
logger.error("resultant bytes for {} is null, error converting ", filename);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
command.add("--dpi");
|
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
|
||||||
command.add(dpi);
|
throw new IOException("Python is not installed. Required for WebP conversion.");
|
||||||
ProcessExecutorResult resultProcess =
|
} else if ("webp".equalsIgnoreCase(imageFormat)
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
|
&& CheckProgramInstall.isPythonAvailable()) {
|
||||||
.runCommandWithOutputHandling(command);
|
// Write the output stream to a temp file
|
||||||
|
tempFile = Files.createTempFile("temp_png", ".png");
|
||||||
// Find all WebP files in the output directory
|
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
|
||||||
List<Path> webpFiles =
|
fos.write(result);
|
||||||
Files.walk(tempOutputDir)
|
fos.flush();
|
||||||
.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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
bodyBytes = zipOutputStream.toByteArray();
|
|
||||||
}
|
|
||||||
// Clean up the temporary files
|
|
||||||
Files.deleteIfExists(tempFile);
|
|
||||||
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
|
|
||||||
result = bodyBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (singleImage) {
|
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
|
||||||
String docName = filename + "." + imageFormat;
|
|
||||||
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
|
List<String> command = new ArrayList<>();
|
||||||
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
|
command.add(pythonVersion);
|
||||||
} else {
|
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
|
||||||
String zipFilename = filename + "_convertedToImages.zip";
|
|
||||||
return WebResponseUtils.bytesToWebResponse(
|
// Create a temporary directory for the output WebP files
|
||||||
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
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()) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
logger.error("Error cleaning up temporary files", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.FileOutputStream;
|
import java.io.File;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
@@ -37,59 +38,90 @@ public class ConvertPDFToPDFA {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Convert a PDF to a PDF/A",
|
summary = "Convert a PDF to a PDF/A",
|
||||||
description =
|
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)
|
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
String outputFormat = request.getOutputFormat();
|
String outputFormat = request.getOutputFormat();
|
||||||
|
|
||||||
// Convert MultipartFile to byte[]
|
// Validate input file type
|
||||||
byte[] pdfBytes = inputFile.getBytes();
|
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||||
|
logger.error("Invalid input file type: {}", inputFile.getContentType());
|
||||||
// Save the uploaded file to a temporary location
|
throw new IllegalArgumentException("Input file must be a PDF");
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
|
||||||
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
|
||||||
outputStream.write(pdfBytes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare the output file path
|
// Get the original filename without extension
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||||
|
if (originalFileName == null || originalFileName.trim().isEmpty()) {
|
||||||
// Prepare the ghostscript command
|
originalFileName = "output.pdf";
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
String baseFileName =
|
||||||
|
originalFileName.contains(".")
|
||||||
|
? originalFileName.substring(0, originalFileName.lastIndexOf('.'))
|
||||||
|
: originalFileName;
|
||||||
|
|
||||||
|
Path tempInputFile = null;
|
||||||
|
Path tempOutputDir = null;
|
||||||
|
byte[] fileBytes;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
|
// Save uploaded file to temp location
|
||||||
// Return the optimized PDF as a response
|
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
String outputFilename =
|
inputFile.transferTo(tempInputFile);
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
|
||||||
.replaceFirst("[.][^.]+$", "")
|
// Create temp output directory
|
||||||
+ "_PDFA.pdf";
|
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) {
|
||||||
|
logger.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(
|
return WebResponseUtils.bytesToWebResponse(
|
||||||
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
|
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
// Clean up temporary files
|
||||||
Files.deleteIfExists(tempInputFile);
|
if (tempInputFile != null) {
|
||||||
Files.deleteIfExists(tempOutputFile);
|
Files.deleteIfExists(tempInputFile);
|
||||||
|
}
|
||||||
|
if (tempOutputDir != null) {
|
||||||
|
FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package stirling.software.SPDF.controller.api.converters;
|
package stirling.software.SPDF.controller.api.converters;
|
||||||
|
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
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.Loader;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ContentDisposition;
|
import org.springframework.http.ContentDisposition;
|
||||||
@@ -18,21 +18,23 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import com.opencsv.CSVWriter;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
import stirling.software.SPDF.controller.api.CropController;
|
|
||||||
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
|
|
||||||
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
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
|
@RestController
|
||||||
@RequestMapping("/api/v1/convert")
|
@RequestMapping("/api/v1/convert")
|
||||||
@Tag(name = "Convert", description = "Convert APIs")
|
@Tag(name = "Convert", description = "Convert APIs")
|
||||||
public class ExtractCSVController {
|
public class ExtractCSVController {
|
||||||
|
|
||||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
private static final Logger logger = LoggerFactory.getLogger(ExtractCSVController.class);
|
||||||
|
|
||||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||||
@Operation(
|
@Operation(
|
||||||
@@ -40,57 +42,16 @@ public class ExtractCSVController {
|
|||||||
description =
|
description =
|
||||||
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
"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 {
|
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();
|
StringWriter writer = new StringWriter();
|
||||||
try (CSVWriter csvWriter = new CSVWriter(writer)) {
|
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||||
csvWriter.writeNext(headersList.toArray(new String[0]));
|
CSVFormat format =
|
||||||
for (String record : recordList) {
|
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
||||||
csvWriter.writeNext(record.split("\\|"));
|
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 +68,4 @@ public class ExtractCSVController {
|
|||||||
|
|
||||||
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
@@ -53,6 +52,54 @@ public class CompressController {
|
|||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
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")
|
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Optimize PDF file",
|
summary = "Optimize PDF file",
|
||||||
@@ -75,209 +122,92 @@ public class CompressController {
|
|||||||
autoMode = true;
|
autoMode = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the uploaded file to a temporary location
|
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
|
||||||
long inputFileSize = Files.size(tempInputFile);
|
long inputFileSize = Files.size(tempInputFile);
|
||||||
|
|
||||||
// Prepare the output file path
|
|
||||||
|
|
||||||
Path tempOutputFile = null;
|
Path tempOutputFile = null;
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
try {
|
try {
|
||||||
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||||
// Determine initial optimization level based on expected size reduction, only if in
|
|
||||||
// autoMode
|
|
||||||
if (autoMode) {
|
if (autoMode) {
|
||||||
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||||
if (sizeReductionRatio > 0.7) {
|
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
|
||||||
optimizeLevel = 1;
|
|
||||||
} else if (sizeReductionRatio > 0.5) {
|
|
||||||
optimizeLevel = 2;
|
|
||||||
} else if (sizeReductionRatio > 0.35) {
|
|
||||||
optimizeLevel = 3;
|
|
||||||
} else {
|
|
||||||
optimizeLevel = 3;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean sizeMet = false;
|
boolean sizeMet = false;
|
||||||
while (!sizeMet && optimizeLevel <= 4) {
|
while (!sizeMet && optimizeLevel <= 9) {
|
||||||
// Prepare the Ghostscript command
|
|
||||||
List<String> command = new ArrayList<>();
|
|
||||||
command.add("gs");
|
|
||||||
command.add("-sDEVICE=pdfwrite");
|
|
||||||
command.add("-dCompatibilityLevel=1.5");
|
|
||||||
|
|
||||||
switch (optimizeLevel) {
|
// Apply additional image compression for levels 6-9
|
||||||
case 1:
|
if (optimizeLevel >= 6) {
|
||||||
command.add("-dPDFSETTINGS=/prepress");
|
// Calculate scale factor based on optimization level
|
||||||
break;
|
double scaleFactor =
|
||||||
case 2:
|
switch (optimizeLevel) {
|
||||||
command.add("-dPDFSETTINGS=/printer");
|
case 6 -> 0.9; // 90% of original size
|
||||||
break;
|
case 7 -> 0.8; // 80% of original size
|
||||||
case 3:
|
case 8 -> 0.65; // 70% of original size
|
||||||
command.add("-dPDFSETTINGS=/ebook");
|
case 9 -> 0.5; // 60% of original size
|
||||||
break;
|
default -> 1.0;
|
||||||
case 4:
|
};
|
||||||
command.add("-dPDFSETTINGS=/screen");
|
compressImagesInPDF(tempInputFile, scaleFactor);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
command.add("-dPDFSETTINGS=/default");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
command.add("-dNOPAUSE");
|
// Run QPDF optimization
|
||||||
command.add("-dQUIET");
|
List<String> command = new ArrayList<>();
|
||||||
command.add("-dBATCH");
|
command.add("qpdf");
|
||||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
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(tempInputFile.toString());
|
||||||
|
command.add(tempOutputFile.toString());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode = null;
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
try {
|
||||||
.runCommandWithOutputHandling(command);
|
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);
|
long outputFileSize = Files.size(tempOutputFile);
|
||||||
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
||||||
sizeMet = true;
|
sizeMet = true;
|
||||||
} else {
|
} else {
|
||||||
// Increase optimization level for next iteration
|
optimizeLevel =
|
||||||
optimizeLevel++;
|
incrementOptimizeLevel(
|
||||||
if (autoMode && optimizeLevel > 4) {
|
optimizeLevel, outputFileSize, expectedOutputSize);
|
||||||
logger.info("Skipping level 5 due to bad results in auto mode");
|
if (autoMode && optimizeLevel > 9) {
|
||||||
|
logger.info("Maximum compression level reached in auto mode");
|
||||||
sizeMet = true;
|
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
|
// Read the optimized PDF file
|
||||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||||
Path finalFile = tempOutputFile;
|
Path finalFile = tempOutputFile;
|
||||||
|
|
||||||
// Check if optimized file is larger than the original
|
// Check if optimized file is larger than the original
|
||||||
if (pdfBytes.length > inputFileSize) {
|
if (pdfBytes.length > inputFileSize) {
|
||||||
// Log the occurrence
|
|
||||||
logger.warn(
|
logger.warn(
|
||||||
"Optimized file is larger than the original. Returning the original file instead.");
|
"Optimized file is larger than the original. Returning the original file instead.");
|
||||||
|
|
||||||
// Read the original file again
|
|
||||||
finalFile = tempInputFile;
|
finalFile = tempInputFile;
|
||||||
}
|
}
|
||||||
// Return the optimized PDF as a response
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
@@ -286,10 +216,31 @@ public class CompressController {
|
|||||||
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
|
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
|
||||||
// deleted by multipart file handler deu to transferTo?
|
|
||||||
// Files.deleteIfExists(tempInputFile);
|
|
||||||
Files.deleteIfExists(tempOutputFile);
|
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;
|
||||||
|
logger.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ public class FakeScanControllerWIP {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Repair a PDF file",
|
summary = "Repair a PDF file",
|
||||||
description =
|
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 {
|
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.WebDataBinder;
|
||||||
|
import org.springframework.web.bind.annotation.InitBinder;
|
||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -26,6 +28,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
|
|
||||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||||
|
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@@ -44,6 +47,11 @@ public class MetadataController {
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InitBinder
|
||||||
|
public void initBinder(WebDataBinder binder) {
|
||||||
|
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Update metadata of a PDF file",
|
summary = "Update metadata of a PDF file",
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
package stirling.software.SPDF.controller.api.misc;
|
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.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipOutputStream;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
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.bind.annotation.RestController;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
import io.github.pixee.security.Filenames;
|
import io.github.pixee.security.Filenames;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
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
|
@RestController
|
||||||
@RequestMapping("/api/v1/misc")
|
@RequestMapping("/api/v1/misc")
|
||||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
@Slf4j
|
||||||
public class OCRController {
|
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() {
|
public List<String> getAvailableTesseractLanguages() {
|
||||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||||
File[] files = new File(tessdataDir).listFiles();
|
File[] files = new File(tessdataDir).listFiles();
|
||||||
@@ -54,196 +71,167 @@ public class OCRController {
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
|
||||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
@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(
|
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
List<String> selectedLanguages = request.getLanguages();
|
List<String> languages = request.getLanguages();
|
||||||
Boolean sidecar = request.isSidecar();
|
|
||||||
Boolean deskew = request.isDeskew();
|
|
||||||
Boolean clean = request.isClean();
|
|
||||||
Boolean cleanFinal = request.isCleanFinal();
|
|
||||||
String ocrType = request.getOcrType();
|
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)) {
|
Path tempDir = Files.createTempDirectory("ocr_process");
|
||||||
throw new IOException("ocrRenderType wrong");
|
Path tempInputFile = tempDir.resolve("input.pdf");
|
||||||
}
|
Path tempOutputDir = tempDir.resolve("output");
|
||||||
|
Path tempImagesDir = tempDir.resolve("images");
|
||||||
// Get available Tesseract languages
|
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
||||||
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;
|
|
||||||
|
|
||||||
|
Files.createDirectories(tempOutputDir);
|
||||||
|
Files.createDirectories(tempImagesDir);
|
||||||
|
Process process = null;
|
||||||
try {
|
try {
|
||||||
|
// Save input file
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
|
PDFMergerUtility merger = new PDFMergerUtility();
|
||||||
|
merger.setDestinationFileName(finalOutputFile.toString());
|
||||||
|
|
||||||
// Run OCR Command
|
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||||
String languageOption = String.join("+", selectedLanguages);
|
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||||
|
int pageCount = document.getNumberOfPages();
|
||||||
|
|
||||||
List<String> command =
|
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
||||||
new ArrayList<>(
|
PDPage page = document.getPage(pageNum);
|
||||||
Arrays.asList(
|
boolean hasText = false;
|
||||||
"ocrmypdf",
|
|
||||||
"--verbose",
|
|
||||||
"2",
|
|
||||||
"--output-type",
|
|
||||||
"pdf",
|
|
||||||
"--pdf-renderer",
|
|
||||||
ocrRenderType));
|
|
||||||
|
|
||||||
if (sidecar != null && sidecar) {
|
// Check for existing text
|
||||||
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
try (PDDocument tempDoc = new PDDocument()) {
|
||||||
command.add("--sidecar");
|
tempDoc.addPage(page);
|
||||||
command.add(sidecarTextPath.toString());
|
PDFTextStripper stripper = new PDFTextStripper();
|
||||||
}
|
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
if (deskew != null && deskew) {
|
boolean shouldOcr =
|
||||||
command.add("--deskew");
|
switch (ocrType) {
|
||||||
}
|
case "skip-text" -> !hasText;
|
||||||
if (clean != null && clean) {
|
case "force-ocr" -> true;
|
||||||
command.add("--clean");
|
default -> true;
|
||||||
}
|
};
|
||||||
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)) {
|
|
||||||
|
|
||||||
|
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(
|
// Merge all pages into final PDF
|
||||||
Arrays.asList(
|
merger.mergeDocuments(null);
|
||||||
"--language",
|
|
||||||
languageOption,
|
|
||||||
tempInputFile.toString(),
|
|
||||||
tempOutputFile.toString()));
|
|
||||||
|
|
||||||
// Run CLI command
|
// Read the final PDF file
|
||||||
ProcessExecutorResult result =
|
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
||||||
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
|
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_OCR.pdf";
|
+ "_OCR.pdf";
|
||||||
|
|
||||||
if (sidecar != null && sidecar) {
|
return ResponseEntity.ok()
|
||||||
// Create a zip file containing both the PDF and the text file
|
.header(
|
||||||
String outputZipFilename =
|
"Content-Disposition",
|
||||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
"attachment; filename=\"" + outputFilename + "\"")
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
+ "_OCR.zip";
|
.body(pdfContent);
|
||||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
|
||||||
|
|
||||||
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 {
|
} finally {
|
||||||
// Clean up the temporary files
|
if (process != null) {
|
||||||
Files.deleteIfExists(tempOutputFile);
|
process.destroy();
|
||||||
// Comment out as transferTo makes multipart handle cleanup
|
|
||||||
// Files.deleteIfExists(tempInputFile);
|
|
||||||
if (sidecarTextPath != null) {
|
|
||||||
Files.deleteIfExists(sidecarTextPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,30 +44,29 @@ public class RepairController {
|
|||||||
@Operation(
|
@Operation(
|
||||||
summary = "Repair a PDF file",
|
summary = "Repair a PDF file",
|
||||||
description =
|
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)
|
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
MultipartFile inputFile = request.getFileInput();
|
MultipartFile inputFile = request.getFileInput();
|
||||||
// Save the uploaded file to a temporary location
|
// Save the uploaded file to a temporary location
|
||||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
|
||||||
byte[] pdfBytes = null;
|
byte[] pdfBytes = null;
|
||||||
inputFile.transferTo(tempInputFile.toFile());
|
inputFile.transferTo(tempInputFile.toFile());
|
||||||
try {
|
try {
|
||||||
|
|
||||||
List<String> command = new ArrayList<>();
|
List<String> command = new ArrayList<>();
|
||||||
command.add("gs");
|
command.add("qpdf");
|
||||||
command.add("-o");
|
command.add("--replace-input"); // Automatically fixes problems it can
|
||||||
command.add(tempOutputFile.toString());
|
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||||
command.add("-sDEVICE=pdfwrite");
|
command.add("--object-streams=disable"); // Can help with some corruptions
|
||||||
command.add(tempInputFile.toString());
|
command.add(tempInputFile.toString());
|
||||||
|
|
||||||
ProcessExecutorResult returnCode =
|
ProcessExecutorResult returnCode =
|
||||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||||
.runCommandWithOutputHandling(command);
|
.runCommandWithOutputHandling(command);
|
||||||
|
|
||||||
// Read the optimized PDF file
|
// Read the optimized PDF file
|
||||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
|
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
|
||||||
|
|
||||||
// Return the optimized PDF as a response
|
// Return the optimized PDF as a response
|
||||||
String outputFilename =
|
String outputFilename =
|
||||||
@@ -78,7 +77,6 @@ public class RepairController {
|
|||||||
} finally {
|
} finally {
|
||||||
// Clean up the temporary files
|
// Clean up the temporary files
|
||||||
Files.deleteIfExists(tempInputFile);
|
Files.deleteIfExists(tempInputFile);
|
||||||
Files.deleteIfExists(tempOutputFile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,10 +229,22 @@ public class StampController {
|
|||||||
calculatePositionY(
|
calculatePositionY(
|
||||||
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
|
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.beginText();
|
||||||
contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y));
|
for (int i = 0; i < lines.length; i++) {
|
||||||
contentStream.showText(stampText);
|
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();
|
contentStream.endText();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ public class PipelineProcessor {
|
|||||||
|
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
String apiKey = getApiKeyForUser();
|
String apiKey = getApiKeyForUser();
|
||||||
headers.add("X-API-Key", apiKey);
|
headers.add("X-API-KEY", apiKey);
|
||||||
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
|
||||||
|
|
||||||
// Create HttpEntity with the body and headers
|
// Create HttpEntity with the body and headers
|
||||||
|
|||||||
@@ -322,27 +322,14 @@ public class GetInfoOnPDF {
|
|||||||
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
||||||
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
|
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
|
||||||
encryption.put("KeyLength", pdfEncryption.getLength());
|
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
|
// Add other encryption-related properties as needed
|
||||||
} else {
|
} else {
|
||||||
encryption.put("IsEncrypted", false);
|
encryption.put("IsEncrypted", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||||
|
setNodePermissions(pdfBoxDoc, permissionsNode);
|
||||||
|
|
||||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||||
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||||
@@ -584,6 +571,7 @@ public class GetInfoOnPDF {
|
|||||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||||
jsonOutput.set("Compliancy", compliancy);
|
jsonOutput.set("Compliancy", compliancy);
|
||||||
jsonOutput.set("Encryption", encryption);
|
jsonOutput.set("Encryption", encryption);
|
||||||
|
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
|
||||||
jsonOutput.set("Other", other);
|
jsonOutput.set("Other", other);
|
||||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||||
|
|
||||||
@@ -602,6 +590,24 @@ public class GetInfoOnPDF {
|
|||||||
return null;
|
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) {
|
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||||
if (outline == null) return;
|
if (outline == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -39,10 +39,7 @@ public class PasswordController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
@PostMapping(consumes = "multipart/form-data", value = "/remove-password")
|
||||||
@Operation(
|
@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")
|
||||||
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")
|
|
||||||
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
@@ -52,15 +49,12 @@ public class PasswordController {
|
|||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_password_removed.pdf");
|
+ "_password_removed.pdf");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
@PostMapping(consumes = "multipart/form-data", value = "/add-password")
|
||||||
@Operation(
|
@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")
|
||||||
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")
|
|
||||||
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
|
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
MultipartFile fileInput = request.getFileInput();
|
MultipartFile fileInput = request.getFileInput();
|
||||||
@@ -98,12 +92,12 @@ public class PasswordController {
|
|||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_permissions.pdf");
|
+ "_permissions.pdf");
|
||||||
return WebResponseUtils.pdfDocToWebResponse(
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
document,
|
document,
|
||||||
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
.replaceFirst("[.][^.]+$", "")
|
.replaceFirst("[.][^.]+$", "")
|
||||||
+ "_passworded.pdf");
|
+ "_passworded.pdf");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.security;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cms.CMSProcessable;
|
||||||
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
|
import org.bouncycastle.cms.SignerInformation;
|
||||||
|
import org.bouncycastle.cms.SignerInformationStore;
|
||||||
|
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||||
|
import org.bouncycastle.util.Store;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
||||||
|
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||||
|
import stirling.software.SPDF.service.CertificateValidationService;
|
||||||
|
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/security")
|
||||||
|
@Tag(name = "Security", description = "Security APIs")
|
||||||
|
public class ValidateSignatureController {
|
||||||
|
|
||||||
|
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||||
|
private final CertificateValidationService certValidationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public ValidateSignatureController(
|
||||||
|
CustomPDDocumentFactory pdfDocumentFactory,
|
||||||
|
CertificateValidationService certValidationService) {
|
||||||
|
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||||
|
this.certValidationService = certValidationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(
|
||||||
|
summary = "Validate PDF Digital Signature",
|
||||||
|
description =
|
||||||
|
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO")
|
||||||
|
@PostMapping(value = "/validate-signature")
|
||||||
|
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||||
|
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||||
|
List<SignatureValidationResult> results = new ArrayList<>();
|
||||||
|
MultipartFile file = request.getFileInput();
|
||||||
|
|
||||||
|
// Load custom certificate if provided
|
||||||
|
X509Certificate customCert = null;
|
||||||
|
if (request.getCertFile() != null && !request.getCertFile().isEmpty()) {
|
||||||
|
try (ByteArrayInputStream certStream =
|
||||||
|
new ByteArrayInputStream(request.getCertFile().getBytes())) {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
|
||||||
|
List<PDSignature> signatures = document.getSignatureDictionaries();
|
||||||
|
|
||||||
|
for (PDSignature sig : signatures) {
|
||||||
|
SignatureValidationResult result = new SignatureValidationResult();
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] signedContent = sig.getSignedContent(file.getInputStream());
|
||||||
|
byte[] signatureBytes = sig.getContents(file.getInputStream());
|
||||||
|
|
||||||
|
CMSProcessable content = new CMSProcessableByteArray(signedContent);
|
||||||
|
CMSSignedData signedData = new CMSSignedData(content, signatureBytes);
|
||||||
|
|
||||||
|
Store<X509CertificateHolder> certStore = signedData.getCertificates();
|
||||||
|
SignerInformationStore signerStore = signedData.getSignerInfos();
|
||||||
|
|
||||||
|
for (SignerInformation signer : signerStore.getSigners()) {
|
||||||
|
X509CertificateHolder certHolder =
|
||||||
|
(X509CertificateHolder)
|
||||||
|
certStore.getMatches(signer.getSID()).iterator().next();
|
||||||
|
X509Certificate cert =
|
||||||
|
new JcaX509CertificateConverter().getCertificate(certHolder);
|
||||||
|
|
||||||
|
boolean isValid =
|
||||||
|
signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
|
||||||
|
result.setValid(isValid);
|
||||||
|
|
||||||
|
// Additional validations
|
||||||
|
result.setChainValid(
|
||||||
|
customCert != null
|
||||||
|
? certValidationService
|
||||||
|
.validateCertificateChainWithCustomCert(
|
||||||
|
cert, customCert)
|
||||||
|
: certValidationService.validateCertificateChain(cert));
|
||||||
|
|
||||||
|
result.setTrustValid(
|
||||||
|
customCert != null
|
||||||
|
? certValidationService.validateTrustWithCustomCert(
|
||||||
|
cert, customCert)
|
||||||
|
: certValidationService.validateTrustStore(cert));
|
||||||
|
|
||||||
|
result.setNotRevoked(!certValidationService.isRevoked(cert));
|
||||||
|
result.setNotExpired(!cert.getNotAfter().before(new Date()));
|
||||||
|
|
||||||
|
// Set basic signature info
|
||||||
|
result.setSignerName(sig.getName());
|
||||||
|
result.setSignatureDate(sig.getSignDate().getTime().toString());
|
||||||
|
result.setReason(sig.getReason());
|
||||||
|
result.setLocation(sig.getLocation());
|
||||||
|
|
||||||
|
// Set new certificate details
|
||||||
|
result.setIssuerDN(cert.getIssuerX500Principal().getName());
|
||||||
|
result.setSubjectDN(cert.getSubjectX500Principal().getName());
|
||||||
|
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format
|
||||||
|
result.setValidFrom(cert.getNotBefore().toString());
|
||||||
|
result.setValidUntil(cert.getNotAfter().toString());
|
||||||
|
result.setSignatureAlgorithm(cert.getSigAlgName());
|
||||||
|
|
||||||
|
// Get key size (if possible)
|
||||||
|
try {
|
||||||
|
result.setKeySize(
|
||||||
|
((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// If not RSA or error, set to 0
|
||||||
|
result.setKeySize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.setVersion(String.valueOf(cert.getVersion()));
|
||||||
|
|
||||||
|
// Set key usage
|
||||||
|
List<String> keyUsages = new ArrayList<>();
|
||||||
|
boolean[] keyUsageFlags = cert.getKeyUsage();
|
||||||
|
if (keyUsageFlags != null) {
|
||||||
|
String[] keyUsageLabels = {
|
||||||
|
"Digital Signature", "Non-Repudiation", "Key Encipherment",
|
||||||
|
"Data Encipherment", "Key Agreement", "Certificate Signing",
|
||||||
|
"CRL Signing", "Encipher Only", "Decipher Only"
|
||||||
|
};
|
||||||
|
for (int i = 0; i < keyUsageFlags.length; i++) {
|
||||||
|
if (keyUsageFlags[i]) {
|
||||||
|
keyUsages.add(keyUsageLabels[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.setKeyUsages(keyUsages);
|
||||||
|
|
||||||
|
// Check if self-signed
|
||||||
|
result.setSelfSigned(
|
||||||
|
cert.getSubjectX500Principal()
|
||||||
|
.equals(cert.getIssuerX500Principal()));
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
result.setValid(false);
|
||||||
|
result.setErrorMessage("Signature validation failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
results.add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(results);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ public class WatermarkController {
|
|||||||
float opacity = request.getOpacity();
|
float opacity = request.getOpacity();
|
||||||
int widthSpacer = request.getWidthSpacer();
|
int widthSpacer = request.getWidthSpacer();
|
||||||
int heightSpacer = request.getHeightSpacer();
|
int heightSpacer = request.getHeightSpacer();
|
||||||
|
String customColor = request.getCustomColor();
|
||||||
boolean convertPdfToImage = request.isConvertPDFToImage();
|
boolean convertPdfToImage = request.isConvertPDFToImage();
|
||||||
|
|
||||||
// Load the input PDF
|
// Load the input PDF
|
||||||
@@ -97,7 +98,8 @@ public class WatermarkController {
|
|||||||
widthSpacer,
|
widthSpacer,
|
||||||
heightSpacer,
|
heightSpacer,
|
||||||
fontSize,
|
fontSize,
|
||||||
alphabet);
|
alphabet,
|
||||||
|
customColor);
|
||||||
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
||||||
addImageWatermark(
|
addImageWatermark(
|
||||||
contentStream,
|
contentStream,
|
||||||
@@ -136,7 +138,8 @@ public class WatermarkController {
|
|||||||
int widthSpacer,
|
int widthSpacer,
|
||||||
int heightSpacer,
|
int heightSpacer,
|
||||||
float fontSize,
|
float fontSize,
|
||||||
String alphabet)
|
String alphabet,
|
||||||
|
String colorString)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
String resourceDir = "";
|
String resourceDir = "";
|
||||||
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||||
@@ -173,7 +176,18 @@ public class WatermarkController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
contentStream.setFont(font, fontSize);
|
contentStream.setFont(font, fontSize);
|
||||||
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
|
||||||
|
Color redactColor;
|
||||||
|
try {
|
||||||
|
if (!colorString.startsWith("#")) {
|
||||||
|
colorString = "#" + colorString;
|
||||||
|
}
|
||||||
|
redactColor = Color.decode(colorString);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
|
||||||
|
redactColor = Color.LIGHT_GRAY;
|
||||||
|
}
|
||||||
|
contentStream.setNonStrokingColor(redactColor);
|
||||||
|
|
||||||
String[] textLines = watermarkText.split("\\\\n");
|
String[] textLines = watermarkText.split("\\\\n");
|
||||||
float maxLineWidth = 0;
|
float maxLineWidth = 0;
|
||||||
|
|||||||
@@ -1,327 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.strippers;
|
|
||||||
|
|
||||||
import java.awt.Shape;
|
|
||||||
import java.awt.geom.AffineTransform;
|
|
||||||
import java.awt.geom.Rectangle2D;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.io.Writer;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.apache.fontbox.util.BoundingBox;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
|
||||||
import org.apache.pdfbox.text.PDFTextStripper;
|
|
||||||
import org.apache.pdfbox.text.PDFTextStripperByArea;
|
|
||||||
import org.apache.pdfbox.text.TextPosition;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
|
|
||||||
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
|
|
||||||
* then extracted using a PDFTextStripperByArea object.
|
|
||||||
*
|
|
||||||
* <p>Works best when headers are included in the detected region, to ensure representative text in
|
|
||||||
* every column.
|
|
||||||
*
|
|
||||||
* <p>Based upon DrawPrintTextLocations PDFBox example
|
|
||||||
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
|
|
||||||
*
|
|
||||||
* @author Beldaz
|
|
||||||
*/
|
|
||||||
public class PDFTableStripper extends PDFTextStripper {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This will print the documents data, for each table cell.
|
|
||||||
*
|
|
||||||
* @param args The command line arguments.
|
|
||||||
* @throws IOException If there is an error parsing the document.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* Used in methods derived from DrawPrintTextLocations
|
|
||||||
*/
|
|
||||||
private AffineTransform flipAT;
|
|
||||||
|
|
||||||
private AffineTransform rotateAT;
|
|
||||||
|
|
||||||
/** Regions updated by calls to writeString */
|
|
||||||
private Set<Rectangle2D> boxes;
|
|
||||||
|
|
||||||
// Border to allow when finding intersections
|
|
||||||
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
|
|
||||||
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
|
|
||||||
|
|
||||||
/** Region in which to find table (otherwise whole page) */
|
|
||||||
private Rectangle2D regionArea;
|
|
||||||
|
|
||||||
/** Number of rows in inferred table */
|
|
||||||
private int nRows = 0;
|
|
||||||
|
|
||||||
/** Number of columns in inferred table */
|
|
||||||
private int nCols = 0;
|
|
||||||
|
|
||||||
/** This is the object that does the text extraction */
|
|
||||||
private PDFTextStripperByArea regionStripper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 1D intervals - used for calculateTableRegions()
|
|
||||||
*
|
|
||||||
* @author Beldaz
|
|
||||||
*/
|
|
||||||
public static class Interval {
|
|
||||||
double start;
|
|
||||||
double end;
|
|
||||||
|
|
||||||
public Interval(double start, double end) {
|
|
||||||
this.start = start;
|
|
||||||
this.end = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(Interval col) {
|
|
||||||
if (col.start < start) start = col.start;
|
|
||||||
if (col.end > end) end = col.end;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addTo(Interval x, LinkedList<Interval> columns) {
|
|
||||||
int p = 0;
|
|
||||||
Iterator<Interval> it = columns.iterator();
|
|
||||||
// Find where x should go
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Interval col = it.next();
|
|
||||||
if (x.end >= col.start) {
|
|
||||||
if (x.start <= col.end) { // overlaps
|
|
||||||
x.add(col);
|
|
||||||
it.remove();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
++p;
|
|
||||||
}
|
|
||||||
while (it.hasNext()) {
|
|
||||||
Interval col = it.next();
|
|
||||||
if (x.start > col.end) break;
|
|
||||||
x.add(col);
|
|
||||||
it.remove();
|
|
||||||
}
|
|
||||||
columns.add(p, x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instantiate a new PDFTableStripper object.
|
|
||||||
*
|
|
||||||
* @throws IOException If there is an error loading the properties.
|
|
||||||
*/
|
|
||||||
public PDFTableStripper() throws IOException {
|
|
||||||
super.setShouldSeparateByBeads(false);
|
|
||||||
regionStripper = new PDFTextStripperByArea();
|
|
||||||
regionStripper.setSortByPosition(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Define the region to group text by.
|
|
||||||
*
|
|
||||||
* @param rect The rectangle area to retrieve the text from.
|
|
||||||
*/
|
|
||||||
public void setRegion(Rectangle2D rect) {
|
|
||||||
regionArea = rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getRows() {
|
|
||||||
return nRows;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getColumns() {
|
|
||||||
return nCols;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the text for the region, this should be called after extractTable().
|
|
||||||
*
|
|
||||||
* @return The text that was identified in that region.
|
|
||||||
*/
|
|
||||||
public String getText(int row, int col) {
|
|
||||||
return regionStripper.getTextForRegion("el" + col + "x" + row);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void extractTable(PDPage pdPage) throws IOException {
|
|
||||||
setStartPage(getCurrentPageNo());
|
|
||||||
setEndPage(getCurrentPageNo());
|
|
||||||
|
|
||||||
boxes = new HashSet<Rectangle2D>();
|
|
||||||
// flip y-axis
|
|
||||||
flipAT = new AffineTransform();
|
|
||||||
flipAT.translate(0, pdPage.getBBox().getHeight());
|
|
||||||
flipAT.scale(1, -1);
|
|
||||||
|
|
||||||
// page may be rotated
|
|
||||||
rotateAT = new AffineTransform();
|
|
||||||
int rotation = pdPage.getRotation();
|
|
||||||
if (rotation != 0) {
|
|
||||||
PDRectangle mediaBox = pdPage.getMediaBox();
|
|
||||||
switch (rotation) {
|
|
||||||
case 90:
|
|
||||||
rotateAT.translate(mediaBox.getHeight(), 0);
|
|
||||||
break;
|
|
||||||
case 270:
|
|
||||||
rotateAT.translate(0, mediaBox.getWidth());
|
|
||||||
break;
|
|
||||||
case 180:
|
|
||||||
rotateAT.translate(mediaBox.getWidth(), mediaBox.getHeight());
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
rotateAT.rotate(Math.toRadians(rotation));
|
|
||||||
}
|
|
||||||
// Trigger processing of the document so that writeString is called.
|
|
||||||
try (Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream())) {
|
|
||||||
super.output = dummy;
|
|
||||||
super.processPage(pdPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle2D[][] regions = calculateTableRegions();
|
|
||||||
|
|
||||||
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
|
|
||||||
// regions");
|
|
||||||
for (int i = 0; i < nCols; ++i) {
|
|
||||||
for (int j = 0; j < nRows; ++j) {
|
|
||||||
final Rectangle2D region = regions[i][j];
|
|
||||||
regionStripper.addRegion("el" + i + "x" + j, region);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
regionStripper.extractRegions(pdPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Infer a rectangular grid of regions from the boxes field.
|
|
||||||
*
|
|
||||||
* @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
|
|
||||||
* may have no content.
|
|
||||||
*/
|
|
||||||
private Rectangle2D[][] calculateTableRegions() {
|
|
||||||
|
|
||||||
// Build up a list of all table regions, based upon the populated
|
|
||||||
// regions of boxes field. Treats the horizontal and vertical extents
|
|
||||||
// of each box as distinct
|
|
||||||
LinkedList<Interval> columns = new LinkedList<Interval>();
|
|
||||||
LinkedList<Interval> rows = new LinkedList<Interval>();
|
|
||||||
|
|
||||||
for (Rectangle2D box : boxes) {
|
|
||||||
Interval x = new Interval(box.getMinX(), box.getMaxX());
|
|
||||||
Interval y = new Interval(box.getMinY(), box.getMaxY());
|
|
||||||
|
|
||||||
Interval.addTo(x, columns);
|
|
||||||
Interval.addTo(y, rows);
|
|
||||||
}
|
|
||||||
|
|
||||||
nRows = rows.size();
|
|
||||||
nCols = columns.size();
|
|
||||||
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
|
|
||||||
int i = 0;
|
|
||||||
// Label regions from top left, rather than the transformed orientation
|
|
||||||
for (Interval column : columns) {
|
|
||||||
int j = 0;
|
|
||||||
for (Interval row : rows) {
|
|
||||||
regions[nCols - i - 1][nRows - j - 1] =
|
|
||||||
new Rectangle2D.Double(
|
|
||||||
column.start,
|
|
||||||
row.start,
|
|
||||||
column.end - column.start,
|
|
||||||
row.end - row.start);
|
|
||||||
++j;
|
|
||||||
}
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return regions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register each character's bounding box, updating boxes field to maintain a list of all
|
|
||||||
* distinct groups of characters.
|
|
||||||
*
|
|
||||||
* <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
|
|
||||||
* DrawPrintTextLocations.java, with extra steps at end of main loop
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
|
|
||||||
for (TextPosition text : textPositions) {
|
|
||||||
// glyph space -> user space
|
|
||||||
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
|
|
||||||
AffineTransform at = text.getTextMatrix().createAffineTransform();
|
|
||||||
PDFont font = text.getFont();
|
|
||||||
BoundingBox bbox = font.getBoundingBox();
|
|
||||||
|
|
||||||
// advance width, bbox height (glyph space)
|
|
||||||
float xadvance =
|
|
||||||
font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
|
||||||
Rectangle2D.Float rect =
|
|
||||||
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
|
||||||
|
|
||||||
if (font instanceof PDType3Font) {
|
|
||||||
// bbox and font matrix are unscaled
|
|
||||||
at.concatenate(font.getFontMatrix().createAffineTransform());
|
|
||||||
} else {
|
|
||||||
// bbox and font matrix are already scaled to 1000
|
|
||||||
at.scale(1 / 1000f, 1 / 1000f);
|
|
||||||
}
|
|
||||||
Shape s = at.createTransformedShape(rect);
|
|
||||||
s = flipAT.createTransformedShape(s);
|
|
||||||
s = rotateAT.createTransformedShape(s);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Merge character's bounding box with boxes field
|
|
||||||
//
|
|
||||||
Rectangle2D bounds = s.getBounds2D();
|
|
||||||
// Pad sides to detect almost touching boxes
|
|
||||||
Rectangle2D hitbox = bounds.getBounds2D();
|
|
||||||
hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
|
|
||||||
hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
|
|
||||||
|
|
||||||
// Find all overlapping boxes
|
|
||||||
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
|
|
||||||
for (Rectangle2D box : boxes) {
|
|
||||||
if (box.intersects(hitbox)) {
|
|
||||||
intersectList.add(box);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all touching boxes and update
|
|
||||||
// (NOTE: Potentially this could leave some overlapping boxes un-merged,
|
|
||||||
// but it's sufficient for now and get's fixed up in calculateTableRegions)
|
|
||||||
for (Rectangle2D box : intersectList) {
|
|
||||||
bounds.add(box);
|
|
||||||
boxes.remove(box);
|
|
||||||
}
|
|
||||||
boxes.add(bounds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method does nothing in this derived class, because beads and regions are incompatible.
|
|
||||||
* Beads are ignored when stripping by area.
|
|
||||||
*
|
|
||||||
* @param aShouldSeparateByBeads The new grouping of beads.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
|
|
||||||
|
|
||||||
/** Adapted from PDFTextStripperByArea {@inheritDoc} */
|
|
||||||
@Override
|
|
||||||
protected void processTextPosition(TextPosition text) {
|
|
||||||
if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
|
|
||||||
// skip character
|
|
||||||
} else {
|
|
||||||
super.processTextPosition(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,9 @@ public class DatabaseWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
||||||
model.addAttribute("systemUpdate", backupList);
|
model.addAttribute("backupFiles", backupList);
|
||||||
|
|
||||||
|
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
|
||||||
|
|
||||||
return "database";
|
return "database";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,11 @@ public class HomeWebController {
|
|||||||
return "licenses";
|
return "licenses";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/releases")
|
||||||
|
public String getReleaseNotes(Model model) {
|
||||||
|
return "releases";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String home(Model model) {
|
public String home(Model model) {
|
||||||
model.addAttribute("currentPage", "home");
|
model.addAttribute("currentPage", "home");
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ public class SecurityWebController {
|
|||||||
return "security/cert-sign";
|
return "security/cert-sign";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/validate-signature")
|
||||||
|
@Hidden
|
||||||
|
public String certSignVerifyForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "validate-signature");
|
||||||
|
return "security/validate-signature";
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/remove-cert-sign")
|
@GetMapping("/remove-cert-sign")
|
||||||
@Hidden
|
@Hidden
|
||||||
public String certUnSignForm(Model model) {
|
public String certUnSignForm(Model model) {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ public class ApplicationProperties {
|
|||||||
private int loginAttemptCount;
|
private int loginAttemptCount;
|
||||||
private long loginResetTimeMinutes;
|
private long loginResetTimeMinutes;
|
||||||
private String loginMethod = "all";
|
private String loginMethod = "all";
|
||||||
|
private String customGlobalAPIKey;
|
||||||
|
|
||||||
public Boolean isAltLogin() {
|
public Boolean isAltLogin() {
|
||||||
return saml2.getEnabled() || oauth2.getEnabled();
|
return saml2.getEnabled() || oauth2.getEnabled();
|
||||||
@@ -285,6 +286,7 @@ public class ApplicationProperties {
|
|||||||
public static class AutomaticallyGenerated {
|
public static class AutomaticallyGenerated {
|
||||||
@ToString.Exclude private String key;
|
@ToString.Exclude private String key;
|
||||||
private String UUID;
|
private String UUID;
|
||||||
|
private String appVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -320,12 +322,20 @@ public class ApplicationProperties {
|
|||||||
public static class SessionLimit {
|
public static class SessionLimit {
|
||||||
private int libreOfficeSessionLimit;
|
private int libreOfficeSessionLimit;
|
||||||
private int pdfToHtmlSessionLimit;
|
private int pdfToHtmlSessionLimit;
|
||||||
private int ocrMyPdfSessionLimit;
|
|
||||||
private int pythonOpenCvSessionLimit;
|
private int pythonOpenCvSessionLimit;
|
||||||
private int ghostScriptSessionLimit;
|
|
||||||
private int weasyPrintSessionLimit;
|
private int weasyPrintSessionLimit;
|
||||||
private int installAppSessionLimit;
|
private int installAppSessionLimit;
|
||||||
private int calibreSessionLimit;
|
private int calibreSessionLimit;
|
||||||
|
private int qpdfSessionLimit;
|
||||||
|
private int tesseractSessionLimit;
|
||||||
|
|
||||||
|
public int getQpdfSessionLimit() {
|
||||||
|
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getTesseractSessionLimit() {
|
||||||
|
return tesseractSessionLimit > 0 ? tesseractSessionLimit : 1;
|
||||||
|
}
|
||||||
|
|
||||||
public int getLibreOfficeSessionLimit() {
|
public int getLibreOfficeSessionLimit() {
|
||||||
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
|
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
|
||||||
@@ -335,18 +345,10 @@ public class ApplicationProperties {
|
|||||||
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
|
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getOcrMyPdfSessionLimit() {
|
|
||||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getPythonOpenCvSessionLimit() {
|
public int getPythonOpenCvSessionLimit() {
|
||||||
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
|
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getGhostScriptSessionLimit() {
|
|
||||||
return ghostScriptSessionLimit > 0 ? ghostScriptSessionLimit : 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getWeasyPrintSessionLimit() {
|
public int getWeasyPrintSessionLimit() {
|
||||||
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
|
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
|
||||||
}
|
}
|
||||||
@@ -364,12 +366,20 @@ public class ApplicationProperties {
|
|||||||
public static class TimeoutMinutes {
|
public static class TimeoutMinutes {
|
||||||
private long libreOfficeTimeoutMinutes;
|
private long libreOfficeTimeoutMinutes;
|
||||||
private long pdfToHtmlTimeoutMinutes;
|
private long pdfToHtmlTimeoutMinutes;
|
||||||
private long ocrMyPdfTimeoutMinutes;
|
|
||||||
private long pythonOpenCvTimeoutMinutes;
|
private long pythonOpenCvTimeoutMinutes;
|
||||||
private long ghostScriptTimeoutMinutes;
|
|
||||||
private long weasyPrintTimeoutMinutes;
|
private long weasyPrintTimeoutMinutes;
|
||||||
private long installAppTimeoutMinutes;
|
private long installAppTimeoutMinutes;
|
||||||
private long calibreTimeoutMinutes;
|
private long calibreTimeoutMinutes;
|
||||||
|
private long tesseractTimeoutMinutes;
|
||||||
|
private long qpdfTimeoutMinutes;
|
||||||
|
|
||||||
|
public long getTesseractTimeoutMinutes() {
|
||||||
|
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getQpdfTimeoutMinutes() {
|
||||||
|
return qpdfTimeoutMinutes > 0 ? qpdfTimeoutMinutes : 30;
|
||||||
|
}
|
||||||
|
|
||||||
public long getLibreOfficeTimeoutMinutes() {
|
public long getLibreOfficeTimeoutMinutes() {
|
||||||
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
|
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
|
||||||
@@ -379,18 +389,10 @@ public class ApplicationProperties {
|
|||||||
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
|
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getOcrMyPdfTimeoutMinutes() {
|
|
||||||
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getPythonOpenCvTimeoutMinutes() {
|
public long getPythonOpenCvTimeoutMinutes() {
|
||||||
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
|
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getGhostScriptTimeoutMinutes() {
|
|
||||||
return ghostScriptTimeoutMinutes > 0 ? ghostScriptTimeoutMinutes : 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getWeasyPrintTimeoutMinutes() {
|
public long getWeasyPrintTimeoutMinutes() {
|
||||||
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
|
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ package stirling.software.SPDF.model;
|
|||||||
|
|
||||||
public enum AuthenticationType {
|
public enum AuthenticationType {
|
||||||
WEB,
|
WEB,
|
||||||
OAUTH2
|
SSO
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,15 @@ public class OptimizePdfRequest extends PDFFile {
|
|||||||
|
|
||||||
@Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.")
|
@Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.")
|
||||||
private String expectedOutputSize;
|
private String expectedOutputSize;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "Whether to linearize the PDF for faster web viewing. Default is false.",
|
||||||
|
defaultValue = "false")
|
||||||
|
private Boolean linearize = false;
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description =
|
||||||
|
"Whether to normalize the PDF content for better compatibility. Default is true.",
|
||||||
|
defaultValue = "true")
|
||||||
|
private Boolean normalize = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,6 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
|||||||
@Schema(description = "List of languages to use in OCR processing")
|
@Schema(description = "List of languages to use in OCR processing")
|
||||||
private List<String> languages;
|
private List<String> languages;
|
||||||
|
|
||||||
@Schema(description = "Include OCR text in a sidecar text file if set to true")
|
|
||||||
private boolean sidecar;
|
|
||||||
|
|
||||||
@Schema(description = "Deskew the input file if set to true")
|
|
||||||
private boolean deskew;
|
|
||||||
|
|
||||||
@Schema(description = "Clean the input file if set to true")
|
|
||||||
private boolean clean;
|
|
||||||
|
|
||||||
@Schema(description = "Clean the final output if set to true")
|
|
||||||
private boolean cleanFinal;
|
|
||||||
|
|
||||||
@Schema(
|
@Schema(
|
||||||
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
||||||
allowableValues = {"skip-text", "force-ocr", "Normal"})
|
allowableValues = {"skip-text", "force-ocr", "Normal"})
|
||||||
@@ -37,7 +25,4 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
|||||||
allowableValues = {"hocr", "sandwich"},
|
allowableValues = {"hocr", "sandwich"},
|
||||||
defaultValue = "hocr")
|
defaultValue = "hocr")
|
||||||
private String ocrRenderType = "hocr";
|
private String ocrRenderType = "hocr";
|
||||||
|
|
||||||
@Schema(description = "Remove images from the output PDF if set to true")
|
|
||||||
private boolean removeImagesAfter;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ public class AddWatermarkRequest extends PDFFile {
|
|||||||
@Schema(description = "The height spacer between watermark elements", example = "50")
|
@Schema(description = "The height spacer between watermark elements", example = "50")
|
||||||
private int heightSpacer;
|
private int heightSpacer;
|
||||||
|
|
||||||
|
@Schema(description = "The color for watermark", defaultValue = "#d3d3d3")
|
||||||
|
private String customColor = "#d3d3d3";
|
||||||
|
|
||||||
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
||||||
private boolean convertPDFToImage;
|
private boolean convertPDFToImage;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import stirling.software.SPDF.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class SignatureValidationRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(description = "(Optional) file to compare PDF cert signatures against x.509 format")
|
||||||
|
private MultipartFile certFile;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package stirling.software.SPDF.model.api.security;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class SignatureValidationResult {
|
||||||
|
private boolean valid;
|
||||||
|
private String signerName;
|
||||||
|
private String signatureDate;
|
||||||
|
private String reason;
|
||||||
|
private String location;
|
||||||
|
private String errorMessage;
|
||||||
|
private boolean chainValid;
|
||||||
|
private boolean trustValid;
|
||||||
|
private boolean notExpired;
|
||||||
|
private boolean notRevoked;
|
||||||
|
|
||||||
|
private String issuerDN; // Certificate issuer's Distinguished Name
|
||||||
|
private String subjectDN; // Certificate subject's Distinguished Name
|
||||||
|
private String serialNumber; // Certificate serial number
|
||||||
|
private String validFrom; // Certificate validity start date
|
||||||
|
private String validUntil; // Certificate validity end date
|
||||||
|
private String signatureAlgorithm; // Algorithm used for signing
|
||||||
|
private int keySize; // Key size in bits
|
||||||
|
private String version; // Certificate version
|
||||||
|
private List<String> keyUsages; // List of key usage purposes
|
||||||
|
private boolean isSelfSigned; // Whether the certificate is self-signed
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package stirling.software.SPDF.pdf;
|
||||||
|
|
||||||
|
import org.apache.commons.csv.CSVFormat;
|
||||||
|
|
||||||
|
import technology.tabula.writers.CSVWriter;
|
||||||
|
|
||||||
|
public class FlexibleCSVWriter extends CSVWriter {
|
||||||
|
|
||||||
|
public FlexibleCSVWriter() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlexibleCSVWriter(CSVFormat csvFormat) {
|
||||||
|
super(csvFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.repository;
|
package stirling.software.SPDF.repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -19,4 +20,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
|||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
|
||||||
Optional<User> findByApiKey(String apiKey);
|
Optional<User> findByApiKey(String apiKey);
|
||||||
|
|
||||||
|
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.cert.CertPath;
|
||||||
|
import java.security.cert.CertPathValidator;
|
||||||
|
import java.security.cert.CertificateExpiredException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.CertificateNotYetValidException;
|
||||||
|
import java.security.cert.PKIXParameters;
|
||||||
|
import java.security.cert.TrustAnchor;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import io.github.pixee.security.BoundedLineReader;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CertificateValidationService {
|
||||||
|
private KeyStore trustStore;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void initializeTrustStore() throws Exception {
|
||||||
|
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||||
|
trustStore.load(null, null);
|
||||||
|
loadMozillaCertificates();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMozillaCertificates() throws Exception {
|
||||||
|
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||||
|
String line;
|
||||||
|
StringBuilder certData = new StringBuilder();
|
||||||
|
boolean inCert = false;
|
||||||
|
int certCount = 0;
|
||||||
|
|
||||||
|
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
||||||
|
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
|
||||||
|
inCert = true;
|
||||||
|
certData = new StringBuilder();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inCert) {
|
||||||
|
if ("END".equals(line)) {
|
||||||
|
inCert = false;
|
||||||
|
byte[] certBytes = parseOctalData(certData.toString());
|
||||||
|
if (certBytes != null) {
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
X509Certificate cert =
|
||||||
|
(X509Certificate)
|
||||||
|
cf.generateCertificate(
|
||||||
|
new ByteArrayInputStream(certBytes));
|
||||||
|
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
certData.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] parseOctalData(String data) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
String[] tokens = data.split("\\\\");
|
||||||
|
for (String token : tokens) {
|
||||||
|
token = token.trim();
|
||||||
|
if (!token.isEmpty()) {
|
||||||
|
baos.write(Integer.parseInt(token, 8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baos.toByteArray();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateCertificateChain(X509Certificate cert) {
|
||||||
|
try {
|
||||||
|
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
|
||||||
|
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||||
|
List<X509Certificate> certList = Arrays.asList(cert);
|
||||||
|
CertPath certPath = cf.generateCertPath(certList);
|
||||||
|
|
||||||
|
Set<TrustAnchor> anchors = new HashSet<>();
|
||||||
|
Enumeration<String> aliases = trustStore.aliases();
|
||||||
|
while (aliases.hasMoreElements()) {
|
||||||
|
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||||
|
if (trustCert instanceof X509Certificate) {
|
||||||
|
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PKIXParameters params = new PKIXParameters(anchors);
|
||||||
|
params.setRevocationEnabled(false);
|
||||||
|
validator.validate(certPath, params);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateTrustStore(X509Certificate cert) {
|
||||||
|
try {
|
||||||
|
Enumeration<String> aliases = trustStore.aliases();
|
||||||
|
while (aliases.hasMoreElements()) {
|
||||||
|
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||||
|
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (KeyStoreException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRevoked(X509Certificate cert) {
|
||||||
|
try {
|
||||||
|
cert.checkValidity();
|
||||||
|
return false;
|
||||||
|
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateCertificateChainWithCustomCert(
|
||||||
|
X509Certificate cert, X509Certificate customCert) {
|
||||||
|
try {
|
||||||
|
cert.verify(customCert.getPublicKey());
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
|
||||||
|
try {
|
||||||
|
// Compare the issuer of the signature certificate with the custom certificate
|
||||||
|
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ public class MetricsAggregatorService {
|
|||||||
this.postHogService = postHogService;
|
this.postHogService = postHogService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(fixedRate = 900000) // Run every 15 minutes
|
@Scheduled(fixedRate = 7200000) // Run every 2 hours
|
||||||
public void aggregateAndSendMetrics() {
|
public void aggregateAndSendMetrics() {
|
||||||
Map<String, Object> metrics = new HashMap<>();
|
Map<String, Object> metrics = new HashMap<>();
|
||||||
Search.in(meterRegistry)
|
Search.in(meterRegistry)
|
||||||
@@ -32,11 +32,24 @@ public class MetricsAggregatorService {
|
|||||||
.counters()
|
.counters()
|
||||||
.forEach(
|
.forEach(
|
||||||
counter -> {
|
counter -> {
|
||||||
|
String method = counter.getId().getTag("method");
|
||||||
|
String uri = counter.getId().getTag("uri");
|
||||||
|
|
||||||
|
// Skip if either method or uri is null
|
||||||
|
if (method == null || uri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!method.equals("GET") && !method.equals("POST")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Skip URIs that are 2 characters or shorter
|
||||||
|
if (uri.length() <= 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
String key =
|
String key =
|
||||||
String.format(
|
String.format(
|
||||||
"http_requests_%s_%s",
|
"http_requests_%s_%s", method, uri.replace("/", "_"));
|
||||||
counter.getId().getTag("method"),
|
|
||||||
counter.getId().getTag("uri").replace("/", "_"));
|
|
||||||
|
|
||||||
double currentCount = counter.count();
|
double currentCount = counter.count();
|
||||||
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||||
|
|||||||
@@ -17,15 +17,18 @@ public class PdfMetadataService {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final String stirlingPDFLabel;
|
private final String stirlingPDFLabel;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
private final boolean runningEE;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PdfMetadataService(
|
public PdfMetadataService(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
||||||
|
@Qualifier("runningEE") boolean runningEE,
|
||||||
@Autowired(required = false) UserServiceInterface userService) {
|
@Autowired(required = false) UserServiceInterface userService) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.stirlingPDFLabel = stirlingPDFLabel;
|
this.stirlingPDFLabel = stirlingPDFLabel;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.runningEE = runningEE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||||
@@ -61,10 +64,8 @@ public class PdfMetadataService {
|
|||||||
|
|
||||||
String creator = stirlingPDFLabel;
|
String creator = stirlingPDFLabel;
|
||||||
|
|
||||||
if (applicationProperties
|
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||||
.getEnterpriseEdition()
|
&& runningEE) {
|
||||||
.getCustomMetadata()
|
|
||||||
.isAutoUpdateMetadata()) {
|
|
||||||
|
|
||||||
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
||||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||||
@@ -83,10 +84,8 @@ public class PdfMetadataService {
|
|||||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||||
|
|
||||||
String author = pdfMetadata.getAuthor();
|
String author = pdfMetadata.getAuthor();
|
||||||
if (applicationProperties
|
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||||
.getEnterpriseEdition()
|
&& runningEE) {
|
||||||
.getCustomMetadata()
|
|
||||||
.isAutoUpdateMetadata()) {
|
|
||||||
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
||||||
|
|
||||||
if (userService != null) {
|
if (userService != null) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.util.TimeZone;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.posthog.java.PostHog;
|
import com.posthog.java.PostHog;
|
||||||
@@ -26,19 +27,25 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
|||||||
public class PostHogService {
|
public class PostHogService {
|
||||||
private final PostHog postHog;
|
private final PostHog postHog;
|
||||||
private final String uniqueId;
|
private final String uniqueId;
|
||||||
|
private final String appVersion;
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final UserServiceInterface userService;
|
private final UserServiceInterface userService;
|
||||||
|
private final Environment env;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PostHogService(
|
public PostHogService(
|
||||||
PostHog postHog,
|
PostHog postHog,
|
||||||
@Qualifier("UUID") String uuid,
|
@Qualifier("UUID") String uuid,
|
||||||
|
@Qualifier("appVersion") String appVersion,
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
@Autowired(required = false) UserServiceInterface userService) {
|
@Autowired(required = false) UserServiceInterface userService,
|
||||||
|
Environment env) {
|
||||||
this.postHog = postHog;
|
this.postHog = postHog;
|
||||||
this.uniqueId = uuid;
|
this.uniqueId = uuid;
|
||||||
|
this.appVersion = appVersion;
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
|
this.env = env;
|
||||||
captureSystemInfo();
|
captureSystemInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +71,16 @@ public class PostHogService {
|
|||||||
Map<String, Object> metrics = new HashMap<>();
|
Map<String, Object> metrics = new HashMap<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Application version
|
||||||
|
metrics.put("app_version", appVersion);
|
||||||
|
String deploymentType = "JAR"; // default
|
||||||
|
if ("true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"))) {
|
||||||
|
deploymentType = "EXE";
|
||||||
|
} else if (isRunningInDocker()) {
|
||||||
|
deploymentType = "DOCKER";
|
||||||
|
}
|
||||||
|
metrics.put("deployment_type", deploymentType);
|
||||||
|
|
||||||
// System info
|
// System info
|
||||||
metrics.put("os_name", System.getProperty("os.name"));
|
metrics.put("os_name", System.getProperty("os.name"));
|
||||||
metrics.put("os_version", System.getProperty("os.version"));
|
metrics.put("os_version", System.getProperty("os.version"));
|
||||||
@@ -132,7 +149,6 @@ public class PostHogService {
|
|||||||
|
|
||||||
// Docker detection and stats
|
// Docker detection and stats
|
||||||
boolean isDocker = isRunningInDocker();
|
boolean isDocker = isRunningInDocker();
|
||||||
metrics.put("is_docker", isDocker);
|
|
||||||
if (isDocker) {
|
if (isDocker) {
|
||||||
metrics.put("docker_metrics", getDockerMetrics());
|
metrics.put("docker_metrics", getDockerMetrics());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ public class FileToPdf {
|
|||||||
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
||||||
ZipEntry entry = zipIn.getNextEntry();
|
ZipEntry entry = zipIn.getNextEntry();
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
Path filePath = tempUnzippedDir.resolve(entry.getName());
|
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
|
||||||
if (!entry.isDirectory()) {
|
if (!entry.isDirectory()) {
|
||||||
Files.createDirectories(filePath.getParent());
|
Files.createDirectories(filePath.getParent());
|
||||||
if (entry.getName().toLowerCase().endsWith(".html")
|
if (entry.getName().toLowerCase().endsWith(".html")
|
||||||
@@ -175,7 +175,7 @@ public class FileToPdf {
|
|||||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
||||||
ZipEntry entry = zipIn.getNextEntry();
|
ZipEntry entry = zipIn.getNextEntry();
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
Path filePath = tempDirectory.resolve(entry.getName());
|
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
Files.createDirectories(filePath); // Explicitly create the directory structure
|
||||||
} else {
|
} else {
|
||||||
@@ -241,4 +241,14 @@ public class FileToPdf {
|
|||||||
Files.deleteIfExists(tempOutputFile);
|
Files.deleteIfExists(tempOutputFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String sanitizeZipFilename(String entryName) {
|
||||||
|
if (entryName == null || entryName.trim().isEmpty()) {
|
||||||
|
return entryName;
|
||||||
|
}
|
||||||
|
while (entryName.contains("../") || entryName.contains("..\\")) {
|
||||||
|
entryName = entryName.replace("../", "").replace("..\\", "");
|
||||||
|
}
|
||||||
|
return entryName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,15 +88,45 @@ public class GeneralUtils {
|
|||||||
|
|
||||||
public static boolean isURLReachable(String urlStr) {
|
public static boolean isURLReachable(String urlStr) {
|
||||||
try {
|
try {
|
||||||
|
// Parse the URL
|
||||||
URL url = URI.create(urlStr).toURL();
|
URL url = URI.create(urlStr).toURL();
|
||||||
|
|
||||||
|
// Allow only http and https protocols
|
||||||
|
String protocol = url.getProtocol();
|
||||||
|
if (!protocol.equals("http") && !protocol.equals("https")) {
|
||||||
|
return false; // Disallow other protocols
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the host is a local address
|
||||||
|
String host = url.getHost();
|
||||||
|
if (isLocalAddress(host)) {
|
||||||
|
return false; // Exclude local addresses
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the URL is reachable
|
||||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
connection.setRequestMethod("HEAD");
|
connection.setRequestMethod("HEAD");
|
||||||
|
// connection.setConnectTimeout(5000); // Set connection timeout
|
||||||
|
// connection.setReadTimeout(5000); // Set read timeout
|
||||||
int responseCode = connection.getResponseCode();
|
int responseCode = connection.getResponseCode();
|
||||||
return (200 <= responseCode && responseCode <= 399);
|
return (200 <= responseCode && responseCode <= 399);
|
||||||
} catch (MalformedURLException e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false; // Return false in case of any exception
|
||||||
} catch (IOException e) {
|
}
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
private static boolean isLocalAddress(String host) {
|
||||||
|
try {
|
||||||
|
// Resolve DNS to IP address
|
||||||
|
InetAddress address = InetAddress.getByName(host);
|
||||||
|
|
||||||
|
// Check for local addresses
|
||||||
|
return address.isAnyLocalAddress() || // Matches 0.0.0.0 or similar
|
||||||
|
address.isLoopbackAddress() || // Matches 127.0.0.1 or ::1
|
||||||
|
address.isSiteLocalAddress() || // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x
|
||||||
|
address.getHostAddress().startsWith("fe80:"); // Matches link-local IPv6 addresses
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false; // Return false for invalid or unresolved addresses
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,6 +319,10 @@ public class GeneralUtils {
|
|||||||
saveKeyToConfig(id, key, true);
|
saveKeyToConfig(id, key, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void saveKeyToConfig(String id, boolean key) throws IOException {
|
||||||
|
saveKeyToConfig(id, key, true);
|
||||||
|
}
|
||||||
|
|
||||||
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
|
||||||
@@ -307,6 +341,24 @@ public class GeneralUtils {
|
|||||||
settingsYml.save();
|
settingsYml.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
|
||||||
|
throws IOException {
|
||||||
|
Path path = Paths.get("configs", "settings.yml");
|
||||||
|
|
||||||
|
final YamlFile settingsYml = new YamlFile(path.toFile());
|
||||||
|
DumperOptions yamlOptionssettingsYml =
|
||||||
|
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
|
||||||
|
yamlOptionssettingsYml.setSplitLines(false);
|
||||||
|
|
||||||
|
settingsYml.loadWithComments();
|
||||||
|
|
||||||
|
YamlFileWrapper writer = settingsYml.path(id).set(key);
|
||||||
|
if (autoGenerated) {
|
||||||
|
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
|
||||||
|
}
|
||||||
|
settingsYml.save();
|
||||||
|
}
|
||||||
|
|
||||||
public static String generateMachineFingerprint() {
|
public static String generateMachineFingerprint() {
|
||||||
try {
|
try {
|
||||||
// Get the MAC address
|
// Get the MAC address
|
||||||
@@ -349,4 +401,33 @@ public class GeneralUtils {
|
|||||||
return "GenericID";
|
return "GenericID";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
|
||||||
|
if (currentVersion == null || compareVersion == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split versions into components
|
||||||
|
String[] current = currentVersion.split("\\.");
|
||||||
|
String[] compare = compareVersion.split("\\.");
|
||||||
|
|
||||||
|
// Get the length of the shorter version array
|
||||||
|
int length = Math.min(current.length, compare.length);
|
||||||
|
|
||||||
|
// Compare each component
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int currentPart = Integer.parseInt(current[i]);
|
||||||
|
int comparePart = Integer.parseInt(compare[i]);
|
||||||
|
|
||||||
|
if (currentPart > comparePart) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (currentPart < comparePart) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all components so far are equal, the longer version is considered higher
|
||||||
|
return current.length > compare.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ public class ProcessExecutor {
|
|||||||
public enum Processes {
|
public enum Processes {
|
||||||
LIBRE_OFFICE,
|
LIBRE_OFFICE,
|
||||||
PDFTOHTML,
|
PDFTOHTML,
|
||||||
OCR_MY_PDF,
|
|
||||||
PYTHON_OPENCV,
|
PYTHON_OPENCV,
|
||||||
GHOSTSCRIPT,
|
|
||||||
WEASYPRINT,
|
WEASYPRINT,
|
||||||
INSTALL_APP,
|
INSTALL_APP,
|
||||||
CALIBRE
|
CALIBRE,
|
||||||
|
TESSERACT,
|
||||||
|
QPDF
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
|
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
|
||||||
@@ -59,21 +59,11 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getPdfToHtmlSessionLimit();
|
.getPdfToHtmlSessionLimit();
|
||||||
case OCR_MY_PDF ->
|
|
||||||
applicationProperties
|
|
||||||
.getProcessExecutor()
|
|
||||||
.getSessionLimit()
|
|
||||||
.getOcrMyPdfSessionLimit();
|
|
||||||
case PYTHON_OPENCV ->
|
case PYTHON_OPENCV ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getPythonOpenCvSessionLimit();
|
.getPythonOpenCvSessionLimit();
|
||||||
case GHOSTSCRIPT ->
|
|
||||||
applicationProperties
|
|
||||||
.getProcessExecutor()
|
|
||||||
.getSessionLimit()
|
|
||||||
.getGhostScriptSessionLimit();
|
|
||||||
case WEASYPRINT ->
|
case WEASYPRINT ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
@@ -84,6 +74,16 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getSessionLimit()
|
.getSessionLimit()
|
||||||
.getInstallAppSessionLimit();
|
.getInstallAppSessionLimit();
|
||||||
|
case TESSERACT ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getTesseractSessionLimit();
|
||||||
|
case QPDF ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getSessionLimit()
|
||||||
|
.getQpdfSessionLimit();
|
||||||
case CALIBRE ->
|
case CALIBRE ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
@@ -103,21 +103,11 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getPdfToHtmlTimeoutMinutes();
|
.getPdfToHtmlTimeoutMinutes();
|
||||||
case OCR_MY_PDF ->
|
|
||||||
applicationProperties
|
|
||||||
.getProcessExecutor()
|
|
||||||
.getTimeoutMinutes()
|
|
||||||
.getOcrMyPdfTimeoutMinutes();
|
|
||||||
case PYTHON_OPENCV ->
|
case PYTHON_OPENCV ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getPythonOpenCvTimeoutMinutes();
|
.getPythonOpenCvTimeoutMinutes();
|
||||||
case GHOSTSCRIPT ->
|
|
||||||
applicationProperties
|
|
||||||
.getProcessExecutor()
|
|
||||||
.getTimeoutMinutes()
|
|
||||||
.getGhostScriptTimeoutMinutes();
|
|
||||||
case WEASYPRINT ->
|
case WEASYPRINT ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
@@ -128,6 +118,16 @@ public class ProcessExecutor {
|
|||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
.getTimeoutMinutes()
|
.getTimeoutMinutes()
|
||||||
.getInstallAppTimeoutMinutes();
|
.getInstallAppTimeoutMinutes();
|
||||||
|
case TESSERACT ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getTesseractTimeoutMinutes();
|
||||||
|
case QPDF ->
|
||||||
|
applicationProperties
|
||||||
|
.getProcessExecutor()
|
||||||
|
.getTimeoutMinutes()
|
||||||
|
.getQpdfTimeoutMinutes();
|
||||||
case CALIBRE ->
|
case CALIBRE ->
|
||||||
applicationProperties
|
applicationProperties
|
||||||
.getProcessExecutor()
|
.getProcessExecutor()
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package stirling.software.SPDF.utils.propertyeditor;
|
||||||
|
|
||||||
|
import java.beans.PropertyEditorSupport;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAsText(String text) throws IllegalArgumentException {
|
||||||
|
try {
|
||||||
|
TypeReference<HashMap<String, String>> typeRef =
|
||||||
|
new TypeReference<HashMap<String, String>>() {};
|
||||||
|
Map<String, String> map = objectMapper.readValue(text, typeRef);
|
||||||
|
setValue(map);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Failed to convert java.lang.String to java.util.Map");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@ multipart.enabled=true
|
|||||||
logging.level.org.springframework=WARN
|
logging.level.org.springframework=WARN
|
||||||
logging.level.org.hibernate=WARN
|
logging.level.org.hibernate=WARN
|
||||||
logging.level.org.eclipse.jetty=WARN
|
logging.level.org.eclipse.jetty=WARN
|
||||||
|
#logging.level.org.springframework.security.saml2=TRACE
|
||||||
|
#logging.level.org.springframework.security=DEBUG
|
||||||
|
#logging.level.org.opensaml=DEBUG
|
||||||
|
#logging.level.stirling.software.SPDF.config.security: DEBUG
|
||||||
logging.level.com.zaxxer.hikari=WARN
|
logging.level.com.zaxxer.hikari=WARN
|
||||||
|
|
||||||
spring.jpa.open-in-view=false
|
spring.jpa.open-in-view=false
|
||||||
@@ -27,6 +31,8 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
|||||||
|
|
||||||
spring.devtools.restart.enabled=true
|
spring.devtools.restart.enabled=true
|
||||||
spring.devtools.livereload.enabled=true
|
spring.devtools.livereload.enabled=true
|
||||||
|
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
|
||||||
|
|
||||||
spring.thymeleaf.encoding=UTF-8
|
spring.thymeleaf.encoding=UTF-8
|
||||||
|
|
||||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||||
@@ -35,7 +41,7 @@ spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
|||||||
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
||||||
#spring.thymeleaf.cache=false
|
#spring.thymeleaf.cache=false
|
||||||
|
|
||||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||||
spring.datasource.driver-class-name=org.h2.Driver
|
spring.datasource.driver-class-name=org.h2.Driver
|
||||||
spring.datasource.username=sa
|
spring.datasource.username=sa
|
||||||
spring.datasource.password=
|
spring.datasource.password=
|
||||||
|
|||||||
25972
src/main/resources/certdata.txt
Normal file
25972
src/main/resources/certdata.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -81,10 +81,11 @@ page=صفحة
|
|||||||
pages=صفحات
|
pages=صفحات
|
||||||
loading=جارٍ التحميل...
|
loading=جارٍ التحميل...
|
||||||
addToDoc=إضافة إلى المستند
|
addToDoc=إضافة إلى المستند
|
||||||
|
reset=إعداة ضبط
|
||||||
|
|
||||||
legal.privacy=سياسة الخصوصية
|
legal.privacy=سياسة الخصوصية
|
||||||
legal.terms=شروط الاستخدام
|
legal.terms=شروط الاستخدام
|
||||||
legal.accessibility=Accessibility
|
legal.accessibility=إمكانية الوصول
|
||||||
legal.cookie=سياسة ملفات تعريف الارتباط
|
legal.cookie=سياسة ملفات تعريف الارتباط
|
||||||
legal.impressum=بيان الهوية
|
legal.impressum=بيان الهوية
|
||||||
|
|
||||||
@@ -118,8 +119,8 @@ pipelineOptions.validateButton=تحقق
|
|||||||
########################
|
########################
|
||||||
enterpriseEdition.button=ترقية إلى محترف
|
enterpriseEdition.button=ترقية إلى محترف
|
||||||
enterpriseEdition.warning=هذه الخاصية متوفرة فقط للمستخدمين المحترفين.
|
enterpriseEdition.warning=هذه الخاصية متوفرة فقط للمستخدمين المحترفين.
|
||||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
|
enterpriseEdition.yamlAdvert=يدعم Stirling PDF Pro ملفات الإعدادات YAML وميزات SSO أخرى
|
||||||
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
|
enterpriseEdition.ssoAdvert=هل تبحث عن المزيد من ميزات إدارة المستخدمين؟ اطلع على Stirling PDF Pro
|
||||||
|
|
||||||
|
|
||||||
#################
|
#################
|
||||||
@@ -141,7 +142,7 @@ navbar.language=اللغات
|
|||||||
navbar.settings=إعدادات
|
navbar.settings=إعدادات
|
||||||
navbar.allTools=أدوات
|
navbar.allTools=أدوات
|
||||||
navbar.multiTool=أدوات متعددة
|
navbar.multiTool=أدوات متعددة
|
||||||
navbar.search=Search
|
navbar.search=البحث
|
||||||
navbar.sections.organize=تنظيم
|
navbar.sections.organize=تنظيم
|
||||||
navbar.sections.convertTo=تحويل الى PDF
|
navbar.sections.convertTo=تحويل الى PDF
|
||||||
navbar.sections.convertFrom=تحويل من PDF
|
navbar.sections.convertFrom=تحويل من PDF
|
||||||
@@ -246,8 +247,8 @@ database.fileNotFound=لم يتم العثور على الملف
|
|||||||
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
||||||
database.failedImportFile=فشل استيراد الملف
|
database.failedImportFile=فشل استيراد الملف
|
||||||
|
|
||||||
session.expired=Your session has expired. Please refresh the page and try again.
|
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=تحديث الصفحة
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
@@ -511,16 +512,20 @@ home.splitPdfByChapters.title=تجزئة المستندات PDF حسب الفص
|
|||||||
home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله.
|
home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله.
|
||||||
splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم
|
splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=إستبدال-عكس اللون
|
||||||
replace-color.header=استبدال-إلغاء مirro لون PDF
|
replace-color.header=استبدال-عكس لون PDF
|
||||||
home.replaceColorPdf.title=Replace and Invert Color
|
home.replaceColorPdf.title=إستبدال و عكس الألوان
|
||||||
home.replaceColorPdf.desc=استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف
|
home.replaceColorPdf.desc=استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف
|
||||||
replaceColorPdf.tags=استبدال اللون، عمليات الصفحة، الخلفية، جانب الخادم
|
replaceColorPdf.tags=استبدال اللون، عمليات الصفحة، الخلفية، جانب الخادم
|
||||||
replace-color.selectText.1=خيارات استبدال-إلغاء مirro لون
|
replace-color.selectText.1=خيارات استبدال أو عكس الألوان
|
||||||
replace-color.selectText.2=Default(Default high contrast colors)
|
replace-color.selectText.2=افتراضي(ألوان التباين العالي الافتراضية)
|
||||||
replace-color.selectText.3=خصيصة (ألوان شخصية)
|
replace-color.selectText.3=خصيصة (ألوان شخصية)
|
||||||
replace-color.selectText.4=Full-Invert(Invert all colors)
|
replace-color.selectText.4=عكس كامل(عكس جميع الألوان)
|
||||||
replace-color.selectText.5=خيارات ألوان التباين العالي
|
replace-color.selectText.5=خيارات ألوان التباين العالي
|
||||||
replace-color.selectText.6=نص أبيض على خلفية سوداء
|
replace-color.selectText.6=نص أبيض على خلفية سوداء
|
||||||
replace-color.selectText.7=نص أسود على خلفية بيضاء
|
replace-color.selectText.7=نص أسود على خلفية بيضاء
|
||||||
@@ -817,7 +822,12 @@ sign.save=حفظ توقيع
|
|||||||
sign.personalSigs=توقيعات شخصية
|
sign.personalSigs=توقيعات شخصية
|
||||||
sign.sharedSigs=توقيعات مشتركة
|
sign.sharedSigs=توقيعات مشتركة
|
||||||
sign.noSavedSigs=لم يتم العثور على توقيعات محفوظة
|
sign.noSavedSigs=لم يتم العثور على توقيعات محفوظة
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=إصلاح
|
repair.title=إصلاح
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=وضع التعرف الضوئي على الحروف
|
|||||||
ocr.selectText.11=إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل)
|
ocr.selectText.11=إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل)
|
||||||
ocr.selectText.12=نوع العرض (متقدم)
|
ocr.selectText.12=نوع العرض (متقدم)
|
||||||
ocr.help=يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker
|
ocr.help=يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker
|
||||||
ocr.credit=تستخدم هذه الخدمة OCRmyPDF و Tesseract للتعرف الضوئي على الحروف.
|
ocr.credit=تستخدم هذه الخدمة qpdf و Tesseract للتعرف الضوئي على الحروف.
|
||||||
ocr.submit=معالجة PDF باستخدام OCR
|
ocr.submit=معالجة PDF باستخدام OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=تحويل إلى PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=ضغط
|
compress.title=ضغط
|
||||||
compress.header=ضغط ملف PDF
|
compress.header=ضغط ملف PDF
|
||||||
compress.credit=تستخدم هذه الخدمة OCRmyPDF لضغط / تحسين PDF.
|
compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF.
|
||||||
compress.selectText.1=الوضع اليدوي - من 1 إلى 4
|
compress.selectText.1=الوضع اليدوي - من 1 إلى 4
|
||||||
compress.selectText.2=مستوى التحسين:
|
compress.selectText.2=مستوى التحسين:
|
||||||
compress.selectText.3=4 (رهيب للصور النصية)
|
compress.selectText.3=4 (رهيب للصور النصية)
|
||||||
@@ -934,17 +944,39 @@ pdfOrganiser.placeholder=(مثال: 1,3,2 أو 4-8,2,10-12 أو 2n-1)
|
|||||||
multiTool.title=أداة متعددة PDF
|
multiTool.title=أداة متعددة PDF
|
||||||
multiTool.header=أداة متعددة PDF
|
multiTool.header=أداة متعددة PDF
|
||||||
multiTool.uploadPrompts=اسم الملف
|
multiTool.uploadPrompts=اسم الملف
|
||||||
multiTool.selectAll=Select All
|
multiTool.selectAll=تحديد الكل
|
||||||
multiTool.deselectAll=Deselect All
|
multiTool.deselectAll=إلغاء تحديد الكل
|
||||||
multiTool.selectPages=Page Select
|
multiTool.selectPages=تحديد الصفحة
|
||||||
multiTool.selectedPages=Selected Pages
|
multiTool.selectedPages=الصفحات المحددة
|
||||||
multiTool.page=Page
|
multiTool.page=صفحة
|
||||||
multiTool.deleteSelected=Delete Selected
|
multiTool.deleteSelected=حذف المحدد
|
||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=تصدير
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=تصدير المحدد
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=إدراج فاصل صفحات
|
||||||
|
multiTool.addFile=إضافة ملف
|
||||||
|
multiTool.rotateLeft=تدوير إلى اليسار
|
||||||
|
multiTool.rotateRight=تدوير إلى اليمين
|
||||||
|
multiTool.split=تقسيم
|
||||||
|
multiTool.moveLeft=تحريك إلى اليسار
|
||||||
|
multiTool.moveRight=تحريك إلى اليمين
|
||||||
|
multiTool.delete=حذف
|
||||||
|
multiTool.dragDropMessage=الصفحات المحددة
|
||||||
|
multiTool.undo=تراجع
|
||||||
|
multiTool.redo=إعادة إجراء
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
|
||||||
|
|
||||||
#view pdf
|
#view pdf
|
||||||
viewPdf.title=عرض PDF
|
viewPdf.title=عرض PDF
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=تشفير
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=إضافة علامة مائية
|
watermark.title=إضافة علامة مائية
|
||||||
watermark.header=إضافة علامة مائية
|
watermark.header=إضافة علامة مائية
|
||||||
|
watermark.customColor=لون نص مخصص
|
||||||
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
|
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
|
||||||
watermark.selectText.2=نص العلامة المائية:
|
watermark.selectText.2=نص العلامة المائية:
|
||||||
watermark.selectText.3=حجم الخط:
|
watermark.selectText.3=حجم الخط:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=تغيير
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF إلى PDF/A
|
pdfToPDFA.title=PDF إلى PDF/A
|
||||||
pdfToPDFA.header=PDF إلى PDF/A
|
pdfToPDFA.header=PDF إلى PDF/A
|
||||||
pdfToPDFA.credit=تستخدم هذه الخدمة ghostscript لتحويل PDF/A.
|
pdfToPDFA.credit=تستخدم هذه الخدمة qpdf لتحويل PDF/A.
|
||||||
pdfToPDFA.submit=تحويل
|
pdfToPDFA.submit=تحويل
|
||||||
pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد
|
pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد
|
||||||
pdfToPDFA.outputFormat=تنسيق الإخراج
|
pdfToPDFA.outputFormat=تنسيق الإخراج
|
||||||
@@ -1238,9 +1271,57 @@ splitByChapters.title=تجزئة المستند حسب الفصول
|
|||||||
splitByChapters.header=تجزئة المستند حسب الفصول
|
splitByChapters.header=تجزئة المستند حسب الفصول
|
||||||
splitByChapters.bookmarkLevel=مستوى العلامات التذكارية
|
splitByChapters.bookmarkLevel=مستوى العلامات التذكارية
|
||||||
splitByChapters.includeMetadata=شامل البيانات المرفقة
|
splitByChapters.includeMetadata=شامل البيانات المرفقة
|
||||||
splitByChapters.allowDuplicates=Allow Duplicates
|
splitByChapters.allowDuplicates=السماح بالتكرار
|
||||||
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
|
splitByChapters.desc.1=هذه الأداة تقوم بتقسيم ملف PDF إلى عدة ملفات PDF استناداً إلى بنية فصوله
|
||||||
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
|
splitByChapters.desc.2=مستوى الإشارة المرجعية: اختر مستوى الإشارات المرجعية التي تريد استخدامها للتقسيم (0 للمستوى الأعلى، 1 للمستوى الثاني، وما إلى ذلك)
|
||||||
splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
|
splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
|
||||||
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
|
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
|
||||||
splitByChapters.submit=تقطيع ملف PDF
|
splitByChapters.submit=تقطيع ملف PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=انقر هنا
|
||||||
|
fileChooser.or=أو
|
||||||
|
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
|
||||||
|
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
1327
src/main/resources/messages_az_AZ.properties
Normal file
1327
src/main/resources/messages_az_AZ.properties
Normal file
File diff suppressed because it is too large
Load Diff
@@ -81,6 +81,7 @@ page=Страница
|
|||||||
pages=Страници
|
pages=Страници
|
||||||
loading=Loading...
|
loading=Loading...
|
||||||
addToDoc=Add to Document
|
addToDoc=Add to Document
|
||||||
|
reset=Reset
|
||||||
|
|
||||||
legal.privacy=Политика за поверителност
|
legal.privacy=Политика за поверителност
|
||||||
legal.terms=Правила и условия
|
legal.terms=Правила и условия
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=Разделете PDF по глави
|
|||||||
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
|
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
|
||||||
splitPdfByChapters.tags=разделяне, глави, отметки, организиране
|
splitPdfByChapters.tags=разделяне, глави, отметки, организиране
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Замени-инвертиране-на-цвят
|
replace-color.title=Замени-инвертиране-на-цвят
|
||||||
replace-color.header=Замяна-инвертиране на цвят PDF
|
replace-color.header=Замяна-инвертиране на цвят PDF
|
||||||
@@ -817,7 +822,12 @@ sign.save=Save Signature
|
|||||||
sign.personalSigs=Personal Signatures
|
sign.personalSigs=Personal Signatures
|
||||||
sign.sharedSigs=Shared Signatures
|
sign.sharedSigs=Shared Signatures
|
||||||
sign.noSavedSigs=No saved signatures found
|
sign.noSavedSigs=No saved signatures found
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Поправи
|
repair.title=Поправи
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=OCR режим
|
|||||||
ocr.selectText.11=Премахване на изображения след OCR (Премахва ВСИЧКИ изображения, полезно само ако е част от стъпката на преобразуване)
|
ocr.selectText.11=Премахване на изображения след OCR (Премахва ВСИЧКИ изображения, полезно само ако е част от стъпката на преобразуване)
|
||||||
ocr.selectText.12=Тип изобразяване (Разширен)
|
ocr.selectText.12=Тип изобразяване (Разширен)
|
||||||
ocr.help=Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker
|
ocr.help=Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker
|
||||||
ocr.credit=Тази услуга използва OCRmyPDF и Tesseract за OCR.
|
ocr.credit=Тази услуга използва qpdf и Tesseract за OCR.
|
||||||
ocr.submit=Обработка на PDF чрез OCR
|
ocr.submit=Обработка на PDF чрез OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=Преобразуване към PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Компресиране
|
compress.title=Компресиране
|
||||||
compress.header=Компресиране на PDF
|
compress.header=Компресиране на PDF
|
||||||
compress.credit=Тази услуга използва Ghostscript за PDF компресиране/оптимизиране.
|
compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране.
|
||||||
compress.selectText.1=Ръчен режим - от 1 до 4
|
compress.selectText.1=Ръчен режим - от 1 до 4
|
||||||
compress.selectText.2=Ниво на оптимизация:
|
compress.selectText.2=Ниво на оптимизация:
|
||||||
compress.selectText.3=4 (Ужасно за текстови изображения)
|
compress.selectText.3=4 (Ужасно за текстови изображения)
|
||||||
@@ -943,6 +953,28 @@ multiTool.deleteSelected=Delete Selected
|
|||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=Export
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=Export Selected
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Insert Page Break
|
||||||
|
multiTool.addFile=Add File
|
||||||
|
multiTool.rotateLeft=Rotate Left
|
||||||
|
multiTool.rotateRight=Rotate Right
|
||||||
|
multiTool.split=Split
|
||||||
|
multiTool.moveLeft=Move Left
|
||||||
|
multiTool.moveRight=Move Right
|
||||||
|
multiTool.delete=Delete
|
||||||
|
multiTool.dragDropMessage=Page(s) Selected
|
||||||
|
multiTool.undo=Undo
|
||||||
|
multiTool.redo=Redo
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||||
|
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=Шифроване
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Добавяне на воден знак
|
watermark.title=Добавяне на воден знак
|
||||||
watermark.header=Добавяне на воден знак
|
watermark.header=Добавяне на воден знак
|
||||||
|
watermark.customColor=Персонализиран цвят на текста
|
||||||
watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
|
watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
|
||||||
watermark.selectText.2=Текст на воден знак:
|
watermark.selectText.2=Текст на воден знак:
|
||||||
watermark.selectText.3=Размер на шрифта:
|
watermark.selectText.3=Размер на шрифта:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Промени
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF към PDF/A
|
pdfToPDFA.title=PDF към PDF/A
|
||||||
pdfToPDFA.header=PDF към PDF/A
|
pdfToPDFA.header=PDF към PDF/A
|
||||||
pdfToPDFA.credit=Тази услуга използва ghostscript за PDF/A преобразуване.
|
pdfToPDFA.credit=Тази услуга използва qpdf за PDF/A преобразуване.
|
||||||
pdfToPDFA.submit=Преобразуване
|
pdfToPDFA.submit=Преобразуване
|
||||||
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
|
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
|
||||||
pdfToPDFA.outputFormat=Изходен формат
|
pdfToPDFA.outputFormat=Изходен формат
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Ниво на отметка: Изберете нивот
|
|||||||
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
|
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
|
||||||
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
|
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
|
||||||
splitByChapters.submit=Разделяне на PDF
|
splitByChapters.submit=Разделяне на PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Click
|
||||||
|
fileChooser.or=or
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ page=Pàgina
|
|||||||
pages=Pàgines
|
pages=Pàgines
|
||||||
loading=Carregant...
|
loading=Carregant...
|
||||||
addToDoc=Afegeix al document
|
addToDoc=Afegeix al document
|
||||||
|
reset=Reset
|
||||||
|
|
||||||
legal.privacy=Política de Privacitat
|
legal.privacy=Política de Privacitat
|
||||||
legal.terms=Termes i condicions
|
legal.terms=Termes i condicions
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=Divideix PDF per Capítols
|
|||||||
home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols.
|
home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols.
|
||||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Reemplaça-Inverteix-Color
|
replace-color.title=Reemplaça-Inverteix-Color
|
||||||
replace-color.header=Reemplaça-Inverteix Color en PDF
|
replace-color.header=Reemplaça-Inverteix Color en PDF
|
||||||
@@ -817,7 +822,12 @@ sign.save=Desa Signatura
|
|||||||
sign.personalSigs=Signatures Personals
|
sign.personalSigs=Signatures Personals
|
||||||
sign.sharedSigs=Signatures Compartides
|
sign.sharedSigs=Signatures Compartides
|
||||||
sign.noSavedSigs=No s'han trobat signatures desades
|
sign.noSavedSigs=No s'han trobat signatures desades
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparar
|
repair.title=Reparar
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=Mode OCR
|
|||||||
ocr.selectText.11=Elimina Imatges després de l'OCR (Elimina TOTES les imatges, útil si forma part d'un procés de conversió)
|
ocr.selectText.11=Elimina Imatges després de l'OCR (Elimina TOTES les imatges, útil si forma part d'un procés de conversió)
|
||||||
ocr.selectText.12=Tipus de Renderització (Avançat)
|
ocr.selectText.12=Tipus de Renderització (Avançat)
|
||||||
ocr.help=Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker
|
ocr.help=Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker
|
||||||
ocr.credit=Aquest servei fa servir OCRmyPDF i Tesseract per a OCR.
|
ocr.credit=Aquest servei fa servir qpdf i Tesseract per a OCR.
|
||||||
ocr.submit=Processa PDF amb OCR
|
ocr.submit=Processa PDF amb OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=Converteix a PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Comprimir
|
compress.title=Comprimir
|
||||||
compress.header=Comprimir PDF
|
compress.header=Comprimir PDF
|
||||||
compress.credit=Aquest servei utilitza Ghostscript per a la compressió/optimització de PDF.
|
compress.credit=Aquest servei utilitza qpdf per a la compressió/optimització de PDF.
|
||||||
compress.selectText.1=Mode manual: de l'1 al 4
|
compress.selectText.1=Mode manual: de l'1 al 4
|
||||||
compress.selectText.2=Nivell d'optimització:
|
compress.selectText.2=Nivell d'optimització:
|
||||||
compress.selectText.3=4 (terrible per a imatges de text)
|
compress.selectText.3=4 (terrible per a imatges de text)
|
||||||
@@ -943,6 +953,28 @@ multiTool.deleteSelected=Delete Selected
|
|||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=Export
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=Export Selected
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Insert Page Break
|
||||||
|
multiTool.addFile=Add File
|
||||||
|
multiTool.rotateLeft=Rotate Left
|
||||||
|
multiTool.rotateRight=Rotate Right
|
||||||
|
multiTool.split=Split
|
||||||
|
multiTool.moveLeft=Move Left
|
||||||
|
multiTool.moveRight=Move Right
|
||||||
|
multiTool.delete=Delete
|
||||||
|
multiTool.dragDropMessage=Page(s) Selected
|
||||||
|
multiTool.undo=Undo
|
||||||
|
multiTool.redo=Redo
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||||
|
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=Encripta
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Afegir Marca d'Aigua
|
watermark.title=Afegir Marca d'Aigua
|
||||||
watermark.header=Afegir Marca d'Aigua
|
watermark.header=Afegir Marca d'Aigua
|
||||||
|
watermark.customColor=Color de Text Personalitzat
|
||||||
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
|
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
|
||||||
watermark.selectText.2=Text de la Marca d'Aigua
|
watermark.selectText.2=Text de la Marca d'Aigua
|
||||||
watermark.selectText.3=Mida de la Font:
|
watermark.selectText.3=Mida de la Font:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Canvia
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF a PDF/A
|
pdfToPDFA.title=PDF a PDF/A
|
||||||
pdfToPDFA.header=PDF a PDF/A
|
pdfToPDFA.header=PDF a PDF/A
|
||||||
pdfToPDFA.credit=Utilitza Ghostscript per a la conversió a PDF/A
|
pdfToPDFA.credit=Utilitza qpdf per a la conversió a PDF/A
|
||||||
pdfToPDFA.submit=Converteix
|
pdfToPDFA.submit=Converteix
|
||||||
pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps
|
pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps
|
||||||
pdfToPDFA.outputFormat=Format de sortida
|
pdfToPDFA.outputFormat=Format de sortida
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Nivell de Marcadors: Tria el nivell de marcadors que s'ut
|
|||||||
splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit.
|
splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit.
|
||||||
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
|
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
|
||||||
splitByChapters.submit=Divideix PDF
|
splitByChapters.submit=Divideix PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Click
|
||||||
|
fileChooser.or=or
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ page=Strana
|
|||||||
pages=Strany
|
pages=Strany
|
||||||
loading=Načítání...
|
loading=Načítání...
|
||||||
addToDoc=Přidat do dokumentu
|
addToDoc=Přidat do dokumentu
|
||||||
|
reset=Reset
|
||||||
|
|
||||||
legal.privacy=Politika soukromí
|
legal.privacy=Politika soukromí
|
||||||
legal.terms=Podmínky použití
|
legal.terms=Podmínky použití
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=Rozdělit PDF podle kapitol
|
|||||||
home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol.
|
home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol.
|
||||||
splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace
|
splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=Replace-Invert-Color
|
||||||
replace-color.header=Nahradit inverzní barvu PDF
|
replace-color.header=Nahradit inverzní barvu PDF
|
||||||
@@ -817,7 +822,12 @@ sign.save=Uložit podpis
|
|||||||
sign.personalSigs=Osobní podpisy
|
sign.personalSigs=Osobní podpisy
|
||||||
sign.sharedSigs=Sdílené podpisy
|
sign.sharedSigs=Sdílené podpisy
|
||||||
sign.noSavedSigs=Nenašly se žádné uložené podpisy
|
sign.noSavedSigs=Nenašly se žádné uložené podpisy
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Opravit
|
repair.title=Opravit
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=Režim OCR
|
|||||||
ocr.selectText.11=Odstranit obrázky po OCR (Odstraní VŠECHNY obrázky, užitečné pouze jako součást kroku konverze)
|
ocr.selectText.11=Odstranit obrázky po OCR (Odstraní VŠECHNY obrázky, užitečné pouze jako součást kroku konverze)
|
||||||
ocr.selectText.12=Typ vykreslení (Pokročilé)
|
ocr.selectText.12=Typ vykreslení (Pokročilé)
|
||||||
ocr.help=Prosím, přečtěte si tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker
|
ocr.help=Prosím, přečtěte si tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker
|
||||||
ocr.credit=Tato služba používá OCRmyPDF a Tesseract pro OCR.
|
ocr.credit=Tato služba používá qpdf a Tesseract pro OCR.
|
||||||
ocr.submit=Zpracovat PDF s OCR
|
ocr.submit=Zpracovat PDF s OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=Převést na PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Komprese
|
compress.title=Komprese
|
||||||
compress.header=Komprimovat PDF
|
compress.header=Komprimovat PDF
|
||||||
compress.credit=Tato služba používá Ghostscript pro kompresi/optimalizaci PDF.
|
compress.credit=Tato služba používá qpdf pro kompresi/optimalizaci PDF.
|
||||||
compress.selectText.1=Ruční režim - Od 1 do 4
|
compress.selectText.1=Ruční režim - Od 1 do 4
|
||||||
compress.selectText.2=Úroveň optimalizace:
|
compress.selectText.2=Úroveň optimalizace:
|
||||||
compress.selectText.3=4 (Hrozné pro textové obrázky)
|
compress.selectText.3=4 (Hrozné pro textové obrázky)
|
||||||
@@ -943,6 +953,28 @@ multiTool.deleteSelected=Delete Selected
|
|||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=Export
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=Export Selected
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Insert Page Break
|
||||||
|
multiTool.addFile=Add File
|
||||||
|
multiTool.rotateLeft=Rotate Left
|
||||||
|
multiTool.rotateRight=Rotate Right
|
||||||
|
multiTool.split=Split
|
||||||
|
multiTool.moveLeft=Move Left
|
||||||
|
multiTool.moveRight=Move Right
|
||||||
|
multiTool.delete=Delete
|
||||||
|
multiTool.dragDropMessage=Page(s) Selected
|
||||||
|
multiTool.undo=Undo
|
||||||
|
multiTool.redo=Redo
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||||
|
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=Šifrovat
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Přidat vodoznak
|
watermark.title=Přidat vodoznak
|
||||||
watermark.header=Přidat vodoznak
|
watermark.header=Přidat vodoznak
|
||||||
|
watermark.customColor=Vlastní barva textu
|
||||||
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
|
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
|
||||||
watermark.selectText.2=Text vodoznaku:
|
watermark.selectText.2=Text vodoznaku:
|
||||||
watermark.selectText.3=Velikost písma:
|
watermark.selectText.3=Velikost písma:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Změnit
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF na PDF/A
|
pdfToPDFA.title=PDF na PDF/A
|
||||||
pdfToPDFA.header=PDF na PDF/A
|
pdfToPDFA.header=PDF na PDF/A
|
||||||
pdfToPDFA.credit=Tato služba používá ghostscript pro konverzi do formátu PDF/A
|
pdfToPDFA.credit=Tato služba používá qpdf pro konverzi do formátu PDF/A
|
||||||
pdfToPDFA.submit=Převést
|
pdfToPDFA.submit=Převést
|
||||||
pdfToPDFA.tip=V současné době nepracuje pro více vstupů najednou
|
pdfToPDFA.tip=V současné době nepracuje pro více vstupů najednou
|
||||||
pdfToPDFA.outputFormat=Výstupní formát
|
pdfToPDFA.outputFormat=Výstupní formát
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Úroveň záhlaví: Zvolte úroveň záhlaví pro použit
|
|||||||
splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru.
|
splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru.
|
||||||
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
|
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
|
||||||
splitByChapters.submit=Podělit se PDF
|
splitByChapters.submit=Podělit se PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Click
|
||||||
|
fileChooser.or=or
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ page=Sidenummer
|
|||||||
pages=Sideantal
|
pages=Sideantal
|
||||||
loading=Laster...
|
loading=Laster...
|
||||||
addToDoc=Tilføj til Dokument
|
addToDoc=Tilføj til Dokument
|
||||||
|
reset=Reset
|
||||||
|
|
||||||
legal.privacy=Privacy Policy
|
legal.privacy=Privacy Policy
|
||||||
legal.terms=Vilkår og betingelser
|
legal.terms=Vilkår og betingelser
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=Partitioner PDF efter kapitler
|
|||||||
home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur.
|
home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur.
|
||||||
splitPdfByChapters.tags=partitionering,kapitler,merker,organisering
|
splitPdfByChapters.tags=partitionering,kapitler,merker,organisering
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=Replace-Invert-Color
|
||||||
replace-color.header=Erstat-omgivende Farve PDF
|
replace-color.header=Erstat-omgivende Farve PDF
|
||||||
@@ -817,7 +822,12 @@ sign.save=Gem Signatur
|
|||||||
sign.personalSigs=Personlige Signaturer
|
sign.personalSigs=Personlige Signaturer
|
||||||
sign.sharedSigs=Delte Signaturer
|
sign.sharedSigs=Delte Signaturer
|
||||||
sign.noSavedSigs=Ingen Gemte Signaturer Fundet
|
sign.noSavedSigs=Ingen Gemte Signaturer Fundet
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparér
|
repair.title=Reparér
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=OCR-tilstand
|
|||||||
ocr.selectText.11=Fjern billeder efter OCR (Fjerner ALLE billeder, kun nyttigt hvis det er en del af konverteringstrinnet)
|
ocr.selectText.11=Fjern billeder efter OCR (Fjerner ALLE billeder, kun nyttigt hvis det er en del af konverteringstrinnet)
|
||||||
ocr.selectText.12=Renderingstype (Avanceret)
|
ocr.selectText.12=Renderingstype (Avanceret)
|
||||||
ocr.help=Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker
|
ocr.help=Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker
|
||||||
ocr.credit=Denne tjeneste bruger OCRmyPDF og Tesseract til OCR.
|
ocr.credit=Denne tjeneste bruger qpdf og Tesseract til OCR.
|
||||||
ocr.submit=Behandl PDF med OCR
|
ocr.submit=Behandl PDF med OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=Konvertér til PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Komprimer
|
compress.title=Komprimer
|
||||||
compress.header=Komprimer PDF
|
compress.header=Komprimer PDF
|
||||||
compress.credit=Denne tjeneste bruger Ghostscript til PDF Komprimering/Optimering.
|
compress.credit=Denne tjeneste bruger qpdf til PDF Komprimering/Optimering.
|
||||||
compress.selectText.1=Manuel Tilstand - Fra 1 til 4
|
compress.selectText.1=Manuel Tilstand - Fra 1 til 4
|
||||||
compress.selectText.2=Optimeringsniveau:
|
compress.selectText.2=Optimeringsniveau:
|
||||||
compress.selectText.3=4 (Forfærdelig for tekstbilleder)
|
compress.selectText.3=4 (Forfærdelig for tekstbilleder)
|
||||||
@@ -943,6 +953,28 @@ multiTool.deleteSelected=Delete Selected
|
|||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=Export
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=Export Selected
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Insert Page Break
|
||||||
|
multiTool.addFile=Add File
|
||||||
|
multiTool.rotateLeft=Rotate Left
|
||||||
|
multiTool.rotateRight=Rotate Right
|
||||||
|
multiTool.split=Split
|
||||||
|
multiTool.moveLeft=Move Left
|
||||||
|
multiTool.moveRight=Move Right
|
||||||
|
multiTool.delete=Delete
|
||||||
|
multiTool.dragDropMessage=Page(s) Selected
|
||||||
|
multiTool.undo=Undo
|
||||||
|
multiTool.redo=Redo
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||||
|
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=Kryptér
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Tilføj Vandmærke
|
watermark.title=Tilføj Vandmærke
|
||||||
watermark.header=Tilføj Vandmærke
|
watermark.header=Tilføj Vandmærke
|
||||||
|
watermark.customColor=Brugerdefineret Tekstfarve
|
||||||
watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
|
watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
|
||||||
watermark.selectText.2=Vandmærketekst:
|
watermark.selectText.2=Vandmærketekst:
|
||||||
watermark.selectText.3=Skriftstørrelse:
|
watermark.selectText.3=Skriftstørrelse:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Ændre
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF Til PDF/A
|
pdfToPDFA.title=PDF Til PDF/A
|
||||||
pdfToPDFA.header=PDF Til PDF/A
|
pdfToPDFA.header=PDF Til PDF/A
|
||||||
pdfToPDFA.credit=Denne tjeneste bruger ghostscript til PDF/A-konvertering
|
pdfToPDFA.credit=Denne tjeneste bruger qpdf til PDF/A-konvertering
|
||||||
pdfToPDFA.submit=Konvertér
|
pdfToPDFA.submit=Konvertér
|
||||||
pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang
|
pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang
|
||||||
pdfToPDFA.outputFormat=Outputformat
|
pdfToPDFA.outputFormat=Outputformat
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Bogmærke niveau: Vælg nivået af bogmærker, der skal b
|
|||||||
splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF.
|
splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF.
|
||||||
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
|
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
|
||||||
splitByChapters.submit=Splitter PDF
|
splitByChapters.submit=Splitter PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Click
|
||||||
|
fileChooser.or=or
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ page=Seite
|
|||||||
pages=Seiten
|
pages=Seiten
|
||||||
loading=Laden...
|
loading=Laden...
|
||||||
addToDoc=In Dokument hinzufügen
|
addToDoc=In Dokument hinzufügen
|
||||||
|
reset=Zurücksetzen
|
||||||
|
|
||||||
legal.privacy=Datenschutz
|
legal.privacy=Datenschutz
|
||||||
legal.terms=AGB
|
legal.terms=AGB
|
||||||
@@ -145,8 +146,8 @@ navbar.search=Suche
|
|||||||
navbar.sections.organize=Organisieren
|
navbar.sections.organize=Organisieren
|
||||||
navbar.sections.convertTo=In PDF konvertieren
|
navbar.sections.convertTo=In PDF konvertieren
|
||||||
navbar.sections.convertFrom=Konvertieren von PDF
|
navbar.sections.convertFrom=Konvertieren von PDF
|
||||||
navbar.sections.security=Zeichen und Sicherheit
|
navbar.sections.security=Signieren und Sicherheit
|
||||||
navbar.sections.advance=Fortschrittlich
|
navbar.sections.advance=Erweiterte Funktionen
|
||||||
navbar.sections.edit=Anzeigen und Bearbeiten
|
navbar.sections.edit=Anzeigen und Bearbeiten
|
||||||
navbar.sections.popular=Beliebt
|
navbar.sections.popular=Beliebt
|
||||||
|
|
||||||
@@ -247,7 +248,7 @@ database.fileNullOrEmpty=Datei darf nicht null oder leer sein
|
|||||||
database.failedImportFile=Dateiimport fehlgeschlagen
|
database.failedImportFile=Dateiimport fehlgeschlagen
|
||||||
|
|
||||||
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
||||||
session.refreshPage=Refresh Page
|
session.refreshPage=Seite aktualisieren
|
||||||
|
|
||||||
#############
|
#############
|
||||||
# HOME-PAGE #
|
# HOME-PAGE #
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=PDF-Datei nach Kapiteln aufteilen
|
|||||||
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
|
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
|
||||||
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
|
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
|
||||||
|
|
||||||
|
home.validateSignature.title=PDF-Signatur überprüfen
|
||||||
|
home.validateSignature.desc=Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,digitale signatur,signatur validieren,überprüfen,Zertifikat,cert
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Farbe Ersetzen-Invertieren
|
replace-color.title=Farbe Ersetzen-Invertieren
|
||||||
replace-color.header=Farb-PDF Ersetzen-Invertieren
|
replace-color.header=Farb-PDF Ersetzen-Invertieren
|
||||||
@@ -817,7 +822,12 @@ sign.save=Signature speichern
|
|||||||
sign.personalSigs=Persönliche Signaturen
|
sign.personalSigs=Persönliche Signaturen
|
||||||
sign.sharedSigs=Geteilte Signaturen
|
sign.sharedSigs=Geteilte Signaturen
|
||||||
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
|
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
|
||||||
|
sign.addToAll=Zu allen Seiten hinzufügen
|
||||||
|
sign.delete=Löschen
|
||||||
|
sign.first=Erste Seite
|
||||||
|
sign.last=Letzte Seite
|
||||||
|
sign.next=Nächste Seite
|
||||||
|
sign.previous=Vorherige Seite
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Reparieren
|
repair.title=Reparieren
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=OCR-Modus
|
|||||||
ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts)
|
ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts)
|
||||||
ocr.selectText.12=Rendertyp (Erweitert)
|
ocr.selectText.12=Rendertyp (Erweitert)
|
||||||
ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können
|
ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können
|
||||||
ocr.credit=Dieser Dienst verwendet OCRmyPDF und Tesseract für OCR.
|
ocr.credit=Dieser Dienst verwendet qpdf und Tesseract für OCR.
|
||||||
ocr.submit=PDF mit OCR verarbeiten
|
ocr.submit=PDF mit OCR verarbeiten
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=In PDF konvertieren
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Komprimieren
|
compress.title=Komprimieren
|
||||||
compress.header=PDF komprimieren
|
compress.header=PDF komprimieren
|
||||||
compress.credit=Dieser Dienst verwendet Ghostscript für die PDF-Komprimierung/-Optimierung.
|
compress.credit=Dieser Dienst verwendet qpdf für die PDF-Komprimierung/-Optimierung.
|
||||||
compress.selectText.1=Manueller Modus – Von 1 bis 4
|
compress.selectText.1=Manueller Modus – Von 1 bis 4
|
||||||
compress.selectText.2=Optimierungsstufe:
|
compress.selectText.2=Optimierungsstufe:
|
||||||
compress.selectText.3=4 (Schrecklich für Textbilder)
|
compress.selectText.3=4 (Schrecklich für Textbilder)
|
||||||
@@ -943,8 +953,30 @@ multiTool.deleteSelected=Auswahl löschen
|
|||||||
multiTool.downloadAll=Downloaden
|
multiTool.downloadAll=Downloaden
|
||||||
multiTool.downloadSelected=Auswahl downloaden
|
multiTool.downloadSelected=Auswahl downloaden
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Seitenumbruch einfügen
|
||||||
|
multiTool.addFile=Datei hinzufügen
|
||||||
|
multiTool.rotateLeft=Nach links drehen
|
||||||
|
multiTool.rotateRight=Nach rechts drehen
|
||||||
|
multiTool.split=Teilen
|
||||||
|
multiTool.moveLeft=Nach links verschieben
|
||||||
|
multiTool.moveRight=Nach rechts verschieben
|
||||||
|
multiTool.delete=Löschen
|
||||||
|
multiTool.dragDropMessage=Ausgewählte Seite(n)
|
||||||
|
multiTool.undo=Rückgängig machen
|
||||||
|
multiTool.redo=Wiederherstellen
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen!
|
||||||
|
|
||||||
#view pdf
|
#view pdf
|
||||||
viewPdf.title=PDF anzeigen
|
viewPdf.title=PDF anzeigen
|
||||||
@@ -1034,6 +1066,7 @@ addPassword.submit=Verschlüsseln
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Wasserzeichen hinzufügen
|
watermark.title=Wasserzeichen hinzufügen
|
||||||
watermark.header=Wasserzeichen hinzufügen
|
watermark.header=Wasserzeichen hinzufügen
|
||||||
|
watermark.customColor=Benutzerdefinierte Textfarbe
|
||||||
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
|
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
|
||||||
watermark.selectText.2=Wasserzeichen Text:
|
watermark.selectText.2=Wasserzeichen Text:
|
||||||
watermark.selectText.3=Schriftgröße:
|
watermark.selectText.3=Schriftgröße:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Ändern
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF zu PDF/A
|
pdfToPDFA.title=PDF zu PDF/A
|
||||||
pdfToPDFA.header=PDF zu PDF/A
|
pdfToPDFA.header=PDF zu PDF/A
|
||||||
pdfToPDFA.credit=Dieser Dienst verwendet ghostscript für die PDF/A-Konvertierung
|
pdfToPDFA.credit=Dieser Dienst verwendet qpdf für die PDF/A-Konvertierung
|
||||||
pdfToPDFA.submit=Konvertieren
|
pdfToPDFA.submit=Konvertieren
|
||||||
pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten.
|
pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten.
|
||||||
pdfToPDFA.outputFormat=Ausgabeformat
|
pdfToPDFA.outputFormat=Ausgabeformat
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Lesezeichenebene: Wählen Sie die Ebene der Lesezeichen,
|
|||||||
splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen.
|
splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen.
|
||||||
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
|
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
|
||||||
splitByChapters.submit=PDF teilen
|
splitByChapters.submit=PDF teilen
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Klicken
|
||||||
|
fileChooser.or=oder
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Veröffentlichungen
|
||||||
|
releases.title=Versionshinweise
|
||||||
|
releases.header=Versionshinweise
|
||||||
|
releases.current.version=Aktuelle Version
|
||||||
|
releases.note=Versionshinweise sind nur auf Englisch verfügbar
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=PDF-Signaturen überprüfen
|
||||||
|
validateSignature.header=Digitale Signaturen überprüfen
|
||||||
|
validateSignature.selectPDF=Signierte PDF-Datei auswählen
|
||||||
|
validateSignature.submit=Signaturen überprüfen
|
||||||
|
validateSignature.results=Gültigkeitsprüfungsergebnisse
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Unterzeichner
|
||||||
|
validateSignature.date=Datum
|
||||||
|
validateSignature.reason=Grund
|
||||||
|
validateSignature.location=Ort
|
||||||
|
validateSignature.noSignatures=Keine digitalen Signaturen in diesem Dokument gefunden
|
||||||
|
validateSignature.status.valid=Gültig
|
||||||
|
validateSignature.status.invalid=Ungültig
|
||||||
|
validateSignature.chain.invalid=Zertifikatskettenprüfung fehlgeschlagen - kann die Identität des Unterzeichners nicht verifizieren
|
||||||
|
validateSignature.trust.invalid=Zertifikat nicht im Truststore - Quelle kann nicht verifiziert werden
|
||||||
|
validateSignature.cert.expired=Zertifikat ist abgelaufen
|
||||||
|
validateSignature.cert.revoked=Zertifikat wurde widerrufen
|
||||||
|
validateSignature.signature.info=Signaturinformationen
|
||||||
|
validateSignature.signature=Signatur
|
||||||
|
validateSignature.signature.mathValid=Signatur ist mathematisch gültig ABER:
|
||||||
|
validateSignature.selectCustomCert=Benutzerdefinierte Zertifikatsdatei X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Zertifikat Details
|
||||||
|
validateSignature.cert.issuer=Aussteller
|
||||||
|
validateSignature.cert.subject=Betreff
|
||||||
|
validateSignature.cert.serialNumber=Seriennummer
|
||||||
|
validateSignature.cert.validFrom=Gültig von
|
||||||
|
validateSignature.cert.validUntil=Gültig bis
|
||||||
|
validateSignature.cert.algorithm=Algorithmus
|
||||||
|
validateSignature.cert.keySize=Schlüsselgröße
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Schlüsselverwendung
|
||||||
|
validateSignature.cert.selfSigned=Selbstsigniert
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ page=Σελίδα
|
|||||||
pages=Σελίδες
|
pages=Σελίδες
|
||||||
loading=Φόρτωση...
|
loading=Φόρτωση...
|
||||||
addToDoc=Πρόσθεση στο Εκπομπώματο
|
addToDoc=Πρόσθεση στο Εκπομπώματο
|
||||||
|
reset=Reset
|
||||||
|
|
||||||
legal.privacy=Πολιτική Προνομίους
|
legal.privacy=Πολιτική Προνομίους
|
||||||
legal.terms=Φράσεις Υποχρεωτικότητας
|
legal.terms=Φράσεις Υποχρεωτικότητας
|
||||||
@@ -511,6 +512,10 @@ home.splitPdfByChapters.title=Διχοτομία PDF ανά Περιγραφές
|
|||||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||||
splitPdfByChapters.tags=διχοτομία,περιγραφές,κεφάλαια,συνορία
|
splitPdfByChapters.tags=διχοτομία,περιγραφές,κεφάλαια,συνορία
|
||||||
|
|
||||||
|
home.validateSignature.title=Validate PDF Signature
|
||||||
|
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||||
|
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||||
|
|
||||||
#replace-invert-color
|
#replace-invert-color
|
||||||
replace-color.title=Replace-Invert-Color
|
replace-color.title=Replace-Invert-Color
|
||||||
replace-color.header=Αντικατάσταση-Αντίστροφη Παθωμένη Πίντσουρ
|
replace-color.header=Αντικατάσταση-Αντίστροφη Παθωμένη Πίντσουρ
|
||||||
@@ -817,7 +822,12 @@ sign.save=Αποθήκευση Αλιάσης
|
|||||||
sign.personalSigs=Προσωπικές Αλιάσεις
|
sign.personalSigs=Προσωπικές Αλιάσεις
|
||||||
sign.sharedSigs=Μεταδότες Αλιάσεις
|
sign.sharedSigs=Μεταδότες Αλιάσεις
|
||||||
sign.noSavedSigs=Δεν βρέθηκαν αποθηκευμένες αλιάσεις
|
sign.noSavedSigs=Δεν βρέθηκαν αποθηκευμένες αλιάσεις
|
||||||
|
sign.addToAll=Add to all pages
|
||||||
|
sign.delete=Delete
|
||||||
|
sign.first=First page
|
||||||
|
sign.last=Last page
|
||||||
|
sign.next=Next page
|
||||||
|
sign.previous=Previous page
|
||||||
|
|
||||||
#repair
|
#repair
|
||||||
repair.title=Επιδιόρθωση
|
repair.title=Επιδιόρθωση
|
||||||
@@ -862,7 +872,7 @@ ocr.selectText.10=Λειτουργία OCR
|
|||||||
ocr.selectText.11=Κατάργηση εικόνων μετά το OCR (Καταργεί ΟΛΕΣ τις εικόνες, είναι χρήσιμο μόνο αν αποτελεί μέρος του βήματος μετατροπής)
|
ocr.selectText.11=Κατάργηση εικόνων μετά το OCR (Καταργεί ΟΛΕΣ τις εικόνες, είναι χρήσιμο μόνο αν αποτελεί μέρος του βήματος μετατροπής)
|
||||||
ocr.selectText.12=Τύπος απόδοσης (Για προχωρημένους)
|
ocr.selectText.12=Τύπος απόδοσης (Για προχωρημένους)
|
||||||
ocr.help=Διαβάστε αυτήν την τεκμηρίωση σχετικά με τον τρόπο χρήσης αυτής για άλλες γλώσσες ή/και μη χρήσης σε docker
|
ocr.help=Διαβάστε αυτήν την τεκμηρίωση σχετικά με τον τρόπο χρήσης αυτής για άλλες γλώσσες ή/και μη χρήσης σε docker
|
||||||
ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί OCRmyPDF και Tesseract για OCR.
|
ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf και Tesseract για OCR.
|
||||||
ocr.submit=Επεξεργασία PDF με OCR
|
ocr.submit=Επεξεργασία PDF με OCR
|
||||||
|
|
||||||
|
|
||||||
@@ -886,7 +896,7 @@ fileToPDF.submit=Μετατροπή σε PDF
|
|||||||
#compress
|
#compress
|
||||||
compress.title=Συμπίεση
|
compress.title=Συμπίεση
|
||||||
compress.header=Συμπίεση PDF
|
compress.header=Συμπίεση PDF
|
||||||
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί Ghostscript για PDF Συμπίεση/Βελτιστοποίηση.
|
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF Συμπίεση/Βελτιστοποίηση.
|
||||||
compress.selectText.1=Χειροκίνητη Λειτουργία - Από 1 έως 4
|
compress.selectText.1=Χειροκίνητη Λειτουργία - Από 1 έως 4
|
||||||
compress.selectText.2=Επίπεδο Βελτιστοποίησης:
|
compress.selectText.2=Επίπεδο Βελτιστοποίησης:
|
||||||
compress.selectText.3=4 (Πολύ κακό για εικόνες κειμένου)
|
compress.selectText.3=4 (Πολύ κακό για εικόνες κειμένου)
|
||||||
@@ -943,6 +953,28 @@ multiTool.deleteSelected=Delete Selected
|
|||||||
multiTool.downloadAll=Export
|
multiTool.downloadAll=Export
|
||||||
multiTool.downloadSelected=Export Selected
|
multiTool.downloadSelected=Export Selected
|
||||||
|
|
||||||
|
multiTool.insertPageBreak=Insert Page Break
|
||||||
|
multiTool.addFile=Add File
|
||||||
|
multiTool.rotateLeft=Rotate Left
|
||||||
|
multiTool.rotateRight=Rotate Right
|
||||||
|
multiTool.split=Split
|
||||||
|
multiTool.moveLeft=Move Left
|
||||||
|
multiTool.moveRight=Move Right
|
||||||
|
multiTool.delete=Delete
|
||||||
|
multiTool.dragDropMessage=Page(s) Selected
|
||||||
|
multiTool.undo=Undo
|
||||||
|
multiTool.redo=Redo
|
||||||
|
|
||||||
|
#decrypt
|
||||||
|
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
|
||||||
|
decrypt.cancelled=Operation cancelled for PDF: {0}
|
||||||
|
decrypt.noPassword=No password provided for encrypted PDF: {0}
|
||||||
|
decrypt.invalidPassword=Please try again with the correct password.
|
||||||
|
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
|
||||||
|
decrypt.unexpectedError=There was an error processing the file. Please try again.
|
||||||
|
decrypt.serverError=Server error while decrypting: {0}
|
||||||
|
decrypt.success=File decrypted successfully.
|
||||||
|
|
||||||
#multiTool-advert
|
#multiTool-advert
|
||||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||||
|
|
||||||
@@ -1034,12 +1066,13 @@ addPassword.submit=Κρυπτογράφηση
|
|||||||
#watermark
|
#watermark
|
||||||
watermark.title=Προσθήκη Υδατογραφήματος
|
watermark.title=Προσθήκη Υδατογραφήματος
|
||||||
watermark.header=Προσθήκη Υδατογραφήματος
|
watermark.header=Προσθήκη Υδατογραφήματος
|
||||||
|
watermark.customColor=Προσαρμοσμένο χρώμα κειμένου
|
||||||
watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος:
|
watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος:
|
||||||
watermark.selectText.2=Κείμενο Υδατογραφήματος:
|
watermark.selectText.2=Κείμενο Υδατογραφήματος:
|
||||||
watermark.selectText.3=Μέγεθος Κειμένου:
|
watermark.selectText.3=Μέγεθος Κειμένου:
|
||||||
watermark.selectText.4=Περιστροφή (0-360):
|
watermark.selectText.4=Περιστροφή (0-360):
|
||||||
watermark.selectText.5=widthSpacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
|
watermark.selectText.5=Width Spacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
|
||||||
watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
|
watermark.selectText.6=Height Spacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
|
||||||
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
|
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
|
||||||
watermark.selectText.8=Τύπος Υδατογραφήματος:
|
watermark.selectText.8=Τύπος Υδατογραφήματος:
|
||||||
watermark.selectText.9=Εικόνα Υδατογραφήματος:
|
watermark.selectText.9=Εικόνα Υδατογραφήματος:
|
||||||
@@ -1096,7 +1129,7 @@ changeMetadata.submit=Αλλαγή
|
|||||||
#pdfToPDFA
|
#pdfToPDFA
|
||||||
pdfToPDFA.title=PDF σε PDF/A
|
pdfToPDFA.title=PDF σε PDF/A
|
||||||
pdfToPDFA.header=PDF σε PDF/A
|
pdfToPDFA.header=PDF σε PDF/A
|
||||||
pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί ghostscript για PDF/A μετατροπή
|
pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF/A μετατροπή
|
||||||
pdfToPDFA.submit=Μετατροπή
|
pdfToPDFA.submit=Μετατροπή
|
||||||
pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα
|
pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα
|
||||||
pdfToPDFA.outputFormat=Εξόδος αναμορφώσεων
|
pdfToPDFA.outputFormat=Εξόδος αναμορφώσεων
|
||||||
@@ -1244,3 +1277,51 @@ splitByChapters.desc.2=Επίπεδο Σήμανσης Σκέψης: Επιλέ
|
|||||||
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
|
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
|
||||||
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
|
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
|
||||||
splitByChapters.submit=Διαλύστε το PDF
|
splitByChapters.submit=Διαλύστε το PDF
|
||||||
|
|
||||||
|
#File Chooser
|
||||||
|
fileChooser.click=Click
|
||||||
|
fileChooser.or=or
|
||||||
|
fileChooser.dragAndDrop=Drag & Drop
|
||||||
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
|
|
||||||
|
#release notes
|
||||||
|
releases.footer=Releases
|
||||||
|
releases.title=Release Notes
|
||||||
|
releases.header=Release Notes
|
||||||
|
releases.current.version=Current Release
|
||||||
|
releases.note=Release notes are only available in English
|
||||||
|
|
||||||
|
#Validate Signature
|
||||||
|
validateSignature.title=Validate PDF Signatures
|
||||||
|
validateSignature.header=Validate Digital Signatures
|
||||||
|
validateSignature.selectPDF=Select signed PDF file
|
||||||
|
validateSignature.submit=Validate Signatures
|
||||||
|
validateSignature.results=Validation Results
|
||||||
|
validateSignature.status=Status
|
||||||
|
validateSignature.signer=Signer
|
||||||
|
validateSignature.date=Date
|
||||||
|
validateSignature.reason=Reason
|
||||||
|
validateSignature.location=Location
|
||||||
|
validateSignature.noSignatures=No digital signatures found in this document
|
||||||
|
validateSignature.status.valid=Valid
|
||||||
|
validateSignature.status.invalid=Invalid
|
||||||
|
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||||
|
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||||
|
validateSignature.cert.expired=Certificate has expired
|
||||||
|
validateSignature.cert.revoked=Certificate has been revoked
|
||||||
|
validateSignature.signature.info=Signature Information
|
||||||
|
validateSignature.signature=Signature
|
||||||
|
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||||
|
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||||
|
validateSignature.cert.info=Certificate Details
|
||||||
|
validateSignature.cert.issuer=Issuer
|
||||||
|
validateSignature.cert.subject=Subject
|
||||||
|
validateSignature.cert.serialNumber=Serial Number
|
||||||
|
validateSignature.cert.validFrom=Valid From
|
||||||
|
validateSignature.cert.validUntil=Valid Until
|
||||||
|
validateSignature.cert.algorithm=Algorithm
|
||||||
|
validateSignature.cert.keySize=Key Size
|
||||||
|
validateSignature.cert.version=Version
|
||||||
|
validateSignature.cert.keyUsage=Key Usage
|
||||||
|
validateSignature.cert.selfSigned=Self-Signed
|
||||||
|
validateSignature.cert.bits=bits
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user