mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 13:20:08 +03:00
Compare commits
71
Commits
@@ -5,6 +5,7 @@ frontend/dist
|
||||
frontend/build
|
||||
frontend/.vite
|
||||
frontend/.tauri
|
||||
frontend/src-tauri/target
|
||||
|
||||
# Gradle build artifacts
|
||||
.gradle
|
||||
|
||||
+17
-13
@@ -6,22 +6,27 @@ app: &app
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
|
||||
openapi: &openapi
|
||||
- build.gradle
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- app/(common|core|proprietary)/src/main/java/**
|
||||
- *build
|
||||
- *app
|
||||
|
||||
project: &project
|
||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||
- app/(common|core|proprietary)/build.gradle
|
||||
- 'app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*'
|
||||
- exampleYmlFiles/**
|
||||
- gradle/**
|
||||
- libs/**
|
||||
- testing/**
|
||||
- build.gradle
|
||||
docker: &docker
|
||||
- Dockerfile
|
||||
- Dockerfile.fat
|
||||
- Dockerfile.ultra-lite
|
||||
- ".github/workflows/build.yml"
|
||||
- scripts/init.sh
|
||||
- scripts/init-without-ocr.sh
|
||||
- exampleYmlFiles/**
|
||||
|
||||
project: &project
|
||||
- app/(common|core|proprietary)/src/(main|test)/java/**
|
||||
- *build
|
||||
- "app/(common|core|proprietary)/src/(main|test)/resources/**/!(messages_*.properties|*.md)*"
|
||||
- exampleYmlFiles/**
|
||||
- gradle/**
|
||||
- libs/**
|
||||
- "testing/**/!(requirements*.txt|requirements*.in)*"
|
||||
- *docker
|
||||
- gradle.properties
|
||||
- gradlew
|
||||
- gradlew.bat
|
||||
@@ -29,7 +34,6 @@ project: &project
|
||||
- settings.gradle
|
||||
- frontend/**
|
||||
- docker/**
|
||||
- testing/**
|
||||
|
||||
frontend: &frontend
|
||||
- frontend/**
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{
|
||||
"label_changer": [
|
||||
"Frooodle",
|
||||
"Ludy87",
|
||||
"balazs-szucs"
|
||||
],
|
||||
"repo_devs": [
|
||||
"Frooodle",
|
||||
"sf298",
|
||||
|
||||
@@ -184,3 +184,6 @@
|
||||
- name: "codex"
|
||||
color: "ededed"
|
||||
description: "chatgpt AI generated code"
|
||||
- name: "break-change"
|
||||
color: "FF0000"
|
||||
description: "This PR introduces a breaking API change."
|
||||
|
||||
@@ -27,6 +27,10 @@ Closes #(issue_number)
|
||||
- [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed)
|
||||
- [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
|
||||
|
||||
### Translations (if applicable)
|
||||
|
||||
- [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)
|
||||
|
||||
### UI Changes (if applicable)
|
||||
|
||||
- [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
changelog:
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- break-change
|
||||
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- Bug
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script processes .properties files for localization checks. It compares translation files in a branch with
|
||||
a reference file to ensure consistency. The script performs two main checks:
|
||||
1. Verifies that the number of lines (including comments and empty lines) in the translation files matches the reference file.
|
||||
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
||||
|
||||
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python check_language_properties.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
"""
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_properties.py --reference-file src\main\resources\messages_en_GB.properties --branch "" --files src\main\resources\messages_de_DE.properties src\main\resources\messages_uk_UA.properties
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
|
||||
|
||||
def find_duplicate_keys(file_path):
|
||||
"""
|
||||
Identifies duplicate keys in a .properties file.
|
||||
:param file_path: Path to the .properties file.
|
||||
:return: List of tuples (key, first_occurrence_line, duplicate_line).
|
||||
"""
|
||||
keys = {}
|
||||
duplicates = []
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
for line_number, line in enumerate(file, start=1):
|
||||
stripped_line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not stripped_line or stripped_line.startswith("#"):
|
||||
continue
|
||||
|
||||
# Split the line into key and value
|
||||
if "=" in stripped_line:
|
||||
key, _ = stripped_line.split("=", 1)
|
||||
key = key.strip()
|
||||
|
||||
# Check if the key already exists
|
||||
if key in keys:
|
||||
duplicates.append((key, keys[key], line_number))
|
||||
else:
|
||||
keys[key] = line_number
|
||||
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for properties files (e.g., 200 KB)
|
||||
MAX_FILE_SIZE = 200 * 1024
|
||||
|
||||
|
||||
def parse_properties_file(file_path):
|
||||
"""
|
||||
Parses a .properties file and returns a structured list of its contents.
|
||||
:param file_path: Path to the .properties file.
|
||||
:return: List of dictionaries representing each line in the file.
|
||||
"""
|
||||
properties_list = []
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
for line_number, line in enumerate(file, start=1):
|
||||
stripped_line = line.strip()
|
||||
|
||||
# Handle empty lines
|
||||
if not stripped_line:
|
||||
properties_list.append(
|
||||
{"line_number": line_number, "type": "empty", "content": ""}
|
||||
)
|
||||
continue
|
||||
|
||||
# Handle comments
|
||||
if stripped_line.startswith("#"):
|
||||
properties_list.append(
|
||||
{
|
||||
"line_number": line_number,
|
||||
"type": "comment",
|
||||
"content": stripped_line,
|
||||
}
|
||||
)
|
||||
continue
|
||||
|
||||
# Handle key-value pairs
|
||||
match = re.match(r"^([^=]+)=(.*)$", line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
properties_list.append(
|
||||
{
|
||||
"line_number": line_number,
|
||||
"type": "entry",
|
||||
"key": key.strip(),
|
||||
"value": value.strip(),
|
||||
}
|
||||
)
|
||||
|
||||
return properties_list
|
||||
|
||||
|
||||
def write_json_file(file_path, updated_properties):
|
||||
"""
|
||||
Writes updated properties back to the file in their original format.
|
||||
:param file_path: Path to the .properties file.
|
||||
:param updated_properties: List of updated properties to write.
|
||||
"""
|
||||
updated_lines = {entry["line_number"]: entry for entry in updated_properties}
|
||||
|
||||
# Sort lines by their numbers and retain comments and empty lines
|
||||
all_lines = sorted(set(updated_lines.keys()))
|
||||
|
||||
original_format = []
|
||||
for line in all_lines:
|
||||
if line in updated_lines:
|
||||
entry = updated_lines[line]
|
||||
else:
|
||||
entry = None
|
||||
ref_entry = updated_lines[line]
|
||||
if ref_entry["type"] in ["comment", "empty"]:
|
||||
original_format.append(ref_entry)
|
||||
elif entry is None:
|
||||
# Add missing entries from the reference file
|
||||
original_format.append(ref_entry)
|
||||
elif entry["type"] == "entry":
|
||||
# Replace entries with those from the current JSON
|
||||
original_format.append(entry)
|
||||
|
||||
# Write the updated content back to the file
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
for entry in original_format:
|
||||
if entry["type"] == "comment":
|
||||
file.write(f"{entry['content']}\n")
|
||||
elif entry["type"] == "empty":
|
||||
file.write(f"{entry['content']}\n")
|
||||
elif entry["type"] == "entry":
|
||||
file.write(f"{entry['key']}={entry['value']}\n")
|
||||
|
||||
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
"""
|
||||
Updates missing keys in the translation files based on the reference file.
|
||||
:param reference_file: Path to the reference .properties file.
|
||||
:param file_list: List of translation files to update.
|
||||
:param branch: Branch where the files are located.
|
||||
"""
|
||||
reference_properties = parse_properties_file(reference_file)
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".properties")
|
||||
or not basename_current_file.startswith("messages_")
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_properties_file(os.path.join(branch, file_path))
|
||||
updated_properties = []
|
||||
for ref_entry in reference_properties:
|
||||
ref_entry_copy = copy.deepcopy(ref_entry)
|
||||
for current_entry in current_properties:
|
||||
if current_entry["type"] == "entry":
|
||||
if ref_entry_copy["type"] != "entry":
|
||||
continue
|
||||
if ref_entry_copy["key"].lower() == current_entry["key"].lower():
|
||||
ref_entry_copy["value"] = current_entry["value"]
|
||||
updated_properties.append(ref_entry_copy)
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
|
||||
|
||||
def read_properties(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.read().splitlines()
|
||||
return [""]
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
reference_branch = reference_file.split("/")[0]
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_lines = read_properties(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
|
||||
file_arr = file_list
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.getcwd(), "app", "core", "src", "main", "resources")
|
||||
)
|
||||
|
||||
for file_path in file_arr:
|
||||
file_normpath = os.path.normpath(file_path)
|
||||
absolute_path = os.path.abspath(file_normpath)
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.startswith(base_dir):
|
||||
raise ValueError(f"Unsafe file found: {file_normpath}")
|
||||
# Verify file size before processing
|
||||
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"The file {file_normpath} is too large and could pose a security risk."
|
||||
)
|
||||
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
or (
|
||||
# only local windows command
|
||||
not file_normpath.startswith(
|
||||
os.path.join(
|
||||
"", "app", "core", "src", "main", "resources", "messages_"
|
||||
)
|
||||
)
|
||||
and not file_normpath.startswith(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"app",
|
||||
"core",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"messages_",
|
||||
)
|
||||
)
|
||||
)
|
||||
or not file_normpath.endswith(".properties")
|
||||
or not basename_current_file.startswith("messages_")
|
||||
):
|
||||
continue
|
||||
only_reference_file = False
|
||||
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
|
||||
current_lines = read_properties(os.path.join(branch, file_path))
|
||||
reference_line_count = len(reference_lines)
|
||||
current_line_count = len(current_lines)
|
||||
|
||||
if reference_line_count != current_line_count:
|
||||
report.append("")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
has_differences = True
|
||||
if reference_line_count > current_line_count:
|
||||
report.append(
|
||||
f" - **_Mismatched line count_**: {reference_line_count} (reference) vs {current_line_count} (current). Comments, empty lines, or translation strings are missing."
|
||||
)
|
||||
elif reference_line_count < current_line_count:
|
||||
report.append(
|
||||
f" - **_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."
|
||||
)
|
||||
else:
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys = []
|
||||
reference_keys = []
|
||||
for line in current_lines:
|
||||
if not line.startswith("#") and line != "" and "=" in line:
|
||||
key, _ = line.split("=", 1)
|
||||
current_keys.append(key)
|
||||
for line in reference_lines:
|
||||
if not line.startswith("#") and line != "" and "=" in line:
|
||||
key, _ = line.split("=", 1)
|
||||
reference_keys.append(key)
|
||||
|
||||
current_keys_set = set(current_keys)
|
||||
reference_keys_set = set(reference_keys)
|
||||
missing_keys = current_keys_set.difference(reference_keys_set)
|
||||
extra_keys = reference_keys_set.difference(current_keys_set)
|
||||
missing_keys_list = list(missing_keys)
|
||||
extra_keys_list = list(extra_keys)
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
has_differences = True
|
||||
missing_keys_str = "`, `".join(missing_keys_list)
|
||||
extra_keys_str = "`, `".join(extra_keys_list)
|
||||
report.append("2. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
if missing_keys_list:
|
||||
spaces_keys_list = []
|
||||
for key in missing_keys_list:
|
||||
if " " in key:
|
||||
spaces_keys_list.append(key)
|
||||
if spaces_keys_list:
|
||||
spaces_keys_str = "`, `".join(spaces_keys_list)
|
||||
report.append(
|
||||
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
|
||||
)
|
||||
report.append(
|
||||
f" - **_Extra keys in `{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{basename_reference_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_current_file}`_**."
|
||||
)
|
||||
else:
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
if find_duplicate_keys(os.path.join(branch, file_normpath)):
|
||||
has_differences = True
|
||||
output = "\n".join(
|
||||
[
|
||||
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
|
||||
for key, first, duplicate in find_duplicate_keys(
|
||||
os.path.join(branch, file_normpath)
|
||||
)
|
||||
]
|
||||
)
|
||||
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
report.append(" - duplicate entries were found:")
|
||||
report.append(output)
|
||||
else:
|
||||
report.append("3. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
report.append("")
|
||||
report.append("---")
|
||||
report.append("")
|
||||
if has_differences:
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/app/core/src/main/resources/messages_en_GB.properties)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
help="Actor from PR.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-file",
|
||||
required=True,
|
||||
help="Path to the reference file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--branch",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Branch name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-file",
|
||||
type=str,
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--files",
|
||||
nargs="+",
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Sanitize --actor input to avoid injection attacks
|
||||
if args.actor:
|
||||
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||
|
||||
# Sanitize --branch input to avoid injection attacks
|
||||
if args.branch:
|
||||
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||
|
||||
file_list = args.files
|
||||
if file_list is None:
|
||||
if args.check_file:
|
||||
file_list = [args.check_file]
|
||||
else:
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"app",
|
||||
"core",
|
||||
"src",
|
||||
"main",
|
||||
"resources",
|
||||
"messages_*.properties",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script processes JSON translation files for localization checks. It compares translation files in a branch with
|
||||
Description: This script processes TOML translation files for localization checks. It compares translation files in a branch with
|
||||
a reference file to ensure consistency. The script performs two main checks:
|
||||
1. Verifies that the number of translation keys in the translation files matches the reference file.
|
||||
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
||||
@@ -9,10 +9,10 @@ The script also provides functionality to update the translation files to match
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python check_language_json.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
python check_language_toml.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
"""
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_json.py --reference-file frontend/public/locales/en-GB/translation.json --branch "" --files frontend/public/locales/de-DE/translation.json frontend/public/locales/fr-FR/translation.json
|
||||
# python .github/scripts/check_language_toml.py --reference-file frontend/public/locales/en-GB/translation.toml --branch "" --files frontend/public/locales/de-DE/translation.toml frontend/public/locales/fr-FR/translation.toml
|
||||
|
||||
import copy
|
||||
import glob
|
||||
@@ -20,12 +20,14 @@ import os
|
||||
import argparse
|
||||
import re
|
||||
import json
|
||||
import tomllib # Python 3.11+ (stdlib)
|
||||
import tomli_w # For writing TOML files
|
||||
|
||||
|
||||
def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
"""
|
||||
Identifies duplicate keys in a JSON file (including nested keys).
|
||||
:param file_path: Path to the JSON file.
|
||||
Identifies duplicate keys in a TOML file (including nested keys).
|
||||
:param file_path: Path to the TOML file.
|
||||
:param keys: Dictionary to track keys (used for recursion).
|
||||
:param prefix: Prefix for nested keys.
|
||||
:return: List of tuples (key, first_occurrence_path, duplicate_path).
|
||||
@@ -35,8 +37,9 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
|
||||
duplicates = []
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
# Load TOML file
|
||||
with open(file_path, 'rb') as file:
|
||||
data = tomllib.load(file)
|
||||
|
||||
def process_dict(obj, current_prefix=""):
|
||||
for key, value in obj.items():
|
||||
@@ -54,18 +57,18 @@ def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for JSON files (e.g., 500 KB)
|
||||
# Maximum size for TOML files (e.g., 500 KB)
|
||||
MAX_FILE_SIZE = 500 * 1024
|
||||
|
||||
|
||||
def parse_json_file(file_path):
|
||||
def parse_toml_file(file_path):
|
||||
"""
|
||||
Parses a JSON translation file and returns a flat dictionary of all keys.
|
||||
:param file_path: Path to the JSON file.
|
||||
Parses a TOML translation file and returns a flat dictionary of all keys.
|
||||
:param file_path: Path to the TOML file.
|
||||
:return: Dictionary with flattened keys.
|
||||
"""
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
with open(file_path, 'rb') as file:
|
||||
data = tomllib.load(file)
|
||||
|
||||
def flatten_dict(d, parent_key="", sep="."):
|
||||
items = {}
|
||||
@@ -99,38 +102,37 @@ def unflatten_dict(d, sep="."):
|
||||
return result
|
||||
|
||||
|
||||
def write_json_file(file_path, updated_properties):
|
||||
def write_toml_file(file_path, updated_properties):
|
||||
"""
|
||||
Writes updated properties back to the JSON file.
|
||||
:param file_path: Path to the JSON file.
|
||||
Writes updated properties back to the TOML file.
|
||||
:param file_path: Path to the TOML file.
|
||||
:param updated_properties: Dictionary of updated properties to write.
|
||||
"""
|
||||
nested_data = unflatten_dict(updated_properties)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
json.dump(nested_data, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n") # Add trailing newline
|
||||
with open(file_path, "wb") as file:
|
||||
tomli_w.dump(nested_data, file)
|
||||
|
||||
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
"""
|
||||
Updates missing keys in the translation files based on the reference file.
|
||||
:param reference_file: Path to the reference JSON file.
|
||||
:param reference_file: Path to the reference TOML file.
|
||||
:param file_list: List of translation files to update.
|
||||
:param branch: Branch where the files are located.
|
||||
"""
|
||||
reference_properties = parse_json_file(reference_file)
|
||||
reference_properties = parse_toml_file(reference_file)
|
||||
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".json")
|
||||
or not file_path.endswith(".toml")
|
||||
or not os.path.dirname(file_path).endswith("locales")
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_json_file(os.path.join(branch, file_path))
|
||||
current_properties = parse_toml_file(os.path.join(branch, file_path))
|
||||
updated_properties = {}
|
||||
|
||||
for ref_key, ref_value in reference_properties.items():
|
||||
@@ -141,16 +143,16 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
# Add missing key with reference value
|
||||
updated_properties[ref_key] = ref_value
|
||||
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
write_toml_file(os.path.join(branch, file_path), updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
|
||||
|
||||
def read_json_keys(file_path):
|
||||
def read_toml_keys(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
return parse_json_file(file_path)
|
||||
return parse_toml_file(file_path)
|
||||
return {}
|
||||
|
||||
|
||||
@@ -160,7 +162,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_keys = read_json_keys(reference_file)
|
||||
reference_keys = read_toml_keys(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
@@ -197,12 +199,12 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
):
|
||||
continue
|
||||
|
||||
if not file_normpath.endswith(".json") or basename_current_file != "translation.json":
|
||||
if not file_normpath.endswith(".toml") or basename_current_file != "translation.toml":
|
||||
continue
|
||||
|
||||
only_reference_file = False
|
||||
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
|
||||
current_keys = read_json_keys(os.path.join(branch, file_path))
|
||||
current_keys = read_toml_keys(os.path.join(branch, file_path))
|
||||
reference_key_count = len(reference_keys)
|
||||
current_key_count = len(current_keys)
|
||||
|
||||
@@ -272,7 +274,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
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 [en-GB/translation.json](https://github.com/Stirling-Tools/Stirling-PDF/blob/V2/frontend/public/locales/en-GB/translation.json)"
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/public/locales/en-GB/translation.toml)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
@@ -286,7 +288,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||
parser = argparse.ArgumentParser(description="Find missing keys in TOML translation files")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
@@ -337,9 +339,9 @@ if __name__ == "__main__":
|
||||
"public",
|
||||
"locales",
|
||||
"*",
|
||||
"translation.json",
|
||||
"translation.toml",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
@@ -6,3 +6,4 @@ pillow
|
||||
unoserver
|
||||
opencv-python-headless
|
||||
pre-commit
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
|
||||
@@ -4,201 +4,98 @@
|
||||
#
|
||||
# pip-compile --allow-unsafe --generate-hashes --output-file='.github\scripts\requirements_dev.txt' --strip-extras '.github\scripts\requirements_dev.in'
|
||||
#
|
||||
brotli==1.1.0 \
|
||||
--hash=sha256:03d20af184290887bdea3f0f78c4f737d126c74dc2f3ccadf07e54ceca3bf208 \
|
||||
--hash=sha256:0541e747cce78e24ea12d69176f6a7ddb690e62c425e01d31cc065e69ce55b48 \
|
||||
--hash=sha256:069a121ac97412d1fe506da790b3e69f52254b9df4eb665cd42460c837193354 \
|
||||
--hash=sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419 \
|
||||
--hash=sha256:0b63b949ff929fbc2d6d3ce0e924c9b93c9785d877a21a1b678877ffbbc4423a \
|
||||
--hash=sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128 \
|
||||
--hash=sha256:11d00ed0a83fa22d29bc6b64ef636c4552ebafcef57154b4ddd132f5638fbd1c \
|
||||
--hash=sha256:141bd4d93984070e097521ed07e2575b46f817d08f9fa42b16b9b5f27b5ac088 \
|
||||
--hash=sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9 \
|
||||
--hash=sha256:1ab4fbee0b2d9098c74f3057b2bc055a8bd92ccf02f65944a241b4349229185a \
|
||||
--hash=sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3 \
|
||||
--hash=sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757 \
|
||||
--hash=sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2 \
|
||||
--hash=sha256:224e57f6eac61cc449f498cc5f0e1725ba2071a3d4f48d5d9dffba42db196438 \
|
||||
--hash=sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578 \
|
||||
--hash=sha256:23032ae55523cc7bccb4f6a0bf368cd25ad9bcdcc1990b64a647e7bbcce9cb5b \
|
||||
--hash=sha256:2333e30a5e00fe0fe55903c8832e08ee9c3b1382aacf4db26664a16528d51b4b \
|
||||
--hash=sha256:2954c1c23f81c2eaf0b0717d9380bd348578a94161a65b3a2afc62c86467dd68 \
|
||||
--hash=sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0 \
|
||||
--hash=sha256:2de9d02f5bda03d27ede52e8cfe7b865b066fa49258cbab568720aa5be80a47d \
|
||||
--hash=sha256:2feb1d960f760a575dbc5ab3b1c00504b24caaf6986e2dc2b01c09c87866a943 \
|
||||
--hash=sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd \
|
||||
--hash=sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409 \
|
||||
--hash=sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28 \
|
||||
--hash=sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da \
|
||||
--hash=sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50 \
|
||||
--hash=sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f \
|
||||
--hash=sha256:3d7954194c36e304e1523f55d7042c59dc53ec20dd4e9ea9d151f1b62b4415c0 \
|
||||
--hash=sha256:3ee8a80d67a4334482d9712b8e83ca6b1d9bc7e351931252ebef5d8f7335a547 \
|
||||
--hash=sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180 \
|
||||
--hash=sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0 \
|
||||
--hash=sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d \
|
||||
--hash=sha256:4410f84b33374409552ac9b6903507cdb31cd30d2501fc5ca13d18f73548444a \
|
||||
--hash=sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb \
|
||||
--hash=sha256:4d4a848d1837973bf0f4b5e54e3bec977d99be36a7895c61abb659301b02c112 \
|
||||
--hash=sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc \
|
||||
--hash=sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2 \
|
||||
--hash=sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265 \
|
||||
--hash=sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327 \
|
||||
--hash=sha256:587ca6d3cef6e4e868102672d3bd9dc9698c309ba56d41c2b9c85bbb903cdb95 \
|
||||
--hash=sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec \
|
||||
--hash=sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd \
|
||||
--hash=sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c \
|
||||
--hash=sha256:5e55da2c8724191e5b557f8e18943b1b4839b8efc3ef60d65985bcf6f587dd38 \
|
||||
--hash=sha256:5eeb539606f18a0b232d4ba45adccde4125592f3f636a6182b4a8a436548b914 \
|
||||
--hash=sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0 \
|
||||
--hash=sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a \
|
||||
--hash=sha256:6172447e1b368dcbc458925e5ddaf9113477b0ed542df258d84fa28fc45ceea7 \
|
||||
--hash=sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368 \
|
||||
--hash=sha256:6974f52a02321b36847cd19d1b8e381bf39939c21efd6ee2fc13a28b0d99348c \
|
||||
--hash=sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0 \
|
||||
--hash=sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f \
|
||||
--hash=sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451 \
|
||||
--hash=sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f \
|
||||
--hash=sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8 \
|
||||
--hash=sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e \
|
||||
--hash=sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248 \
|
||||
--hash=sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c \
|
||||
--hash=sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91 \
|
||||
--hash=sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724 \
|
||||
--hash=sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7 \
|
||||
--hash=sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966 \
|
||||
--hash=sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9 \
|
||||
--hash=sha256:890b5a14ce214389b2cc36ce82f3093f96f4cc730c1cffdbefff77a7c71f2a97 \
|
||||
--hash=sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d \
|
||||
--hash=sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5 \
|
||||
--hash=sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf \
|
||||
--hash=sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac \
|
||||
--hash=sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b \
|
||||
--hash=sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951 \
|
||||
--hash=sha256:919e32f147ae93a09fe064d77d5ebf4e35502a8df75c29fb05788528e330fe74 \
|
||||
--hash=sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648 \
|
||||
--hash=sha256:929811df5462e182b13920da56c6e0284af407d1de637d8e536c5cd00a7daf60 \
|
||||
--hash=sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c \
|
||||
--hash=sha256:a090ca607cbb6a34b0391776f0cb48062081f5f60ddcce5d11838e67a01928d1 \
|
||||
--hash=sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8 \
|
||||
--hash=sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d \
|
||||
--hash=sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc \
|
||||
--hash=sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61 \
|
||||
--hash=sha256:a599669fd7c47233438a56936988a2478685e74854088ef5293802123b5b2460 \
|
||||
--hash=sha256:a743e5a28af5f70f9c080380a5f908d4d21d40e8f0e0c8901604d15cfa9ba751 \
|
||||
--hash=sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9 \
|
||||
--hash=sha256:a7e53012d2853a07a4a79c00643832161a910674a893d296c9f1259859a289d2 \
|
||||
--hash=sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0 \
|
||||
--hash=sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1 \
|
||||
--hash=sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474 \
|
||||
--hash=sha256:aea440a510e14e818e67bfc4027880e2fb500c2ccb20ab21c7a7c8b5b4703d75 \
|
||||
--hash=sha256:af6fa6817889314555aede9a919612b23739395ce767fe7fcbea9a80bf140fe5 \
|
||||
--hash=sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f \
|
||||
--hash=sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2 \
|
||||
--hash=sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f \
|
||||
--hash=sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb \
|
||||
--hash=sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6 \
|
||||
--hash=sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9 \
|
||||
--hash=sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111 \
|
||||
--hash=sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2 \
|
||||
--hash=sha256:cb1dac1770878ade83f2ccdf7d25e494f05c9165f5246b46a621cc849341dc01 \
|
||||
--hash=sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467 \
|
||||
--hash=sha256:cdbc1fc1bc0bff1cef838eafe581b55bfbffaed4ed0318b724d0b71d4d377619 \
|
||||
--hash=sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf \
|
||||
--hash=sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408 \
|
||||
--hash=sha256:d143fd47fad1db3d7c27a1b1d66162e855b5d50a89666af46e1679c496e8e579 \
|
||||
--hash=sha256:d192f0f30804e55db0d0e0a35d83a9fead0e9a359a9ed0285dbacea60cc10a84 \
|
||||
--hash=sha256:d2b35ca2c7f81d173d2fadc2f4f31e88cc5f7a39ae5b6db5513cf3383b0e0ec7 \
|
||||
--hash=sha256:d342778ef319e1026af243ed0a07c97acf3bad33b9f29e7ae6a1f68fd083e90c \
|
||||
--hash=sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284 \
|
||||
--hash=sha256:d7702622a8b40c49bffb46e1e3ba2e81268d5c04a34f460978c6b5517a34dd52 \
|
||||
--hash=sha256:db85ecf4e609a48f4b29055f1e144231b90edc90af7481aa731ba2d059226b1b \
|
||||
--hash=sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59 \
|
||||
--hash=sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752 \
|
||||
--hash=sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1 \
|
||||
--hash=sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80 \
|
||||
--hash=sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839 \
|
||||
--hash=sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0 \
|
||||
--hash=sha256:e93dfc1a1165e385cc8239fab7c036fb2cd8093728cbd85097b284d7b99249a2 \
|
||||
--hash=sha256:efa8b278894b14d6da122a72fefcebc28445f2d3f880ac59d46c90f4c13be9a3 \
|
||||
--hash=sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64 \
|
||||
--hash=sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089 \
|
||||
--hash=sha256:f296c40e23065d0d6650c4aefe7470d2a25fffda489bcc3eb66083f3ac9f6643 \
|
||||
--hash=sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b \
|
||||
--hash=sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e \
|
||||
--hash=sha256:f733d788519c7e3e71f0855c96618720f5d3d60c3cb829d8bbb722dddce37985 \
|
||||
--hash=sha256:fce1473f3ccc4187f75b4690cfc922628aed4d3dd013d047f95a9b3919a86596 \
|
||||
--hash=sha256:fd5f17ff8f14003595ab414e45fce13d073e0762394f957182e69035c9f3d7c2 \
|
||||
--hash=sha256:fdc3ff3bfccdc6b9cc7c342c03aa2400683f0cb891d46e94b64a197910dc4064
|
||||
# via fonttools
|
||||
cffi==1.17.1 \
|
||||
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
|
||||
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
|
||||
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
|
||||
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
|
||||
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
|
||||
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
|
||||
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
|
||||
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
|
||||
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
|
||||
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
|
||||
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
|
||||
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
|
||||
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
|
||||
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
|
||||
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
|
||||
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
|
||||
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
|
||||
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
|
||||
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
|
||||
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
|
||||
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
|
||||
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
|
||||
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
|
||||
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
|
||||
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
|
||||
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
|
||||
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
|
||||
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
|
||||
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
|
||||
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
|
||||
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
|
||||
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
|
||||
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
|
||||
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
|
||||
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
|
||||
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
|
||||
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
|
||||
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
|
||||
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
|
||||
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
|
||||
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
|
||||
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
|
||||
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
|
||||
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
|
||||
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
|
||||
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
|
||||
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
|
||||
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
|
||||
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
|
||||
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
|
||||
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
|
||||
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
|
||||
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
|
||||
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
|
||||
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
|
||||
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
|
||||
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
|
||||
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
|
||||
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
|
||||
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
|
||||
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
|
||||
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
|
||||
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
|
||||
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
|
||||
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
|
||||
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
|
||||
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
|
||||
# WARNING: pip install will require the following package to be hashed.
|
||||
# Consider using a hashable URL like https://github.com/jazzband/pip-tools/archive/SOMECOMMIT.zip
|
||||
# CVE-2025-6176 mitigation: pin brotli to a specific commit
|
||||
brotli @ git+https://github.com/google/brotli.git@028fb5a23661f123017c060daa546b55cf4bde29
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# fonttools
|
||||
cffi==2.0.0 \
|
||||
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
|
||||
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
|
||||
--hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \
|
||||
--hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \
|
||||
--hash=sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44 \
|
||||
--hash=sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2 \
|
||||
--hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \
|
||||
--hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \
|
||||
--hash=sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65 \
|
||||
--hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \
|
||||
--hash=sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a \
|
||||
--hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \
|
||||
--hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \
|
||||
--hash=sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a \
|
||||
--hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \
|
||||
--hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \
|
||||
--hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \
|
||||
--hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \
|
||||
--hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \
|
||||
--hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \
|
||||
--hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \
|
||||
--hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \
|
||||
--hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \
|
||||
--hash=sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb \
|
||||
--hash=sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165 \
|
||||
--hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \
|
||||
--hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \
|
||||
--hash=sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c \
|
||||
--hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \
|
||||
--hash=sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c \
|
||||
--hash=sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0 \
|
||||
--hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \
|
||||
--hash=sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63 \
|
||||
--hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \
|
||||
--hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \
|
||||
--hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \
|
||||
--hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \
|
||||
--hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \
|
||||
--hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \
|
||||
--hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \
|
||||
--hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \
|
||||
--hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \
|
||||
--hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \
|
||||
--hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \
|
||||
--hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \
|
||||
--hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \
|
||||
--hash=sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322 \
|
||||
--hash=sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb \
|
||||
--hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \
|
||||
--hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \
|
||||
--hash=sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4 \
|
||||
--hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \
|
||||
--hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \
|
||||
--hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \
|
||||
--hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \
|
||||
--hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \
|
||||
--hash=sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739 \
|
||||
--hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \
|
||||
--hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \
|
||||
--hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \
|
||||
--hash=sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9 \
|
||||
--hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \
|
||||
--hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \
|
||||
--hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \
|
||||
--hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \
|
||||
--hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \
|
||||
--hash=sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f \
|
||||
--hash=sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495 \
|
||||
--hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \
|
||||
--hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \
|
||||
--hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \
|
||||
--hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \
|
||||
--hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \
|
||||
--hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \
|
||||
--hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \
|
||||
--hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \
|
||||
--hash=sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7 \
|
||||
--hash=sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5 \
|
||||
--hash=sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534 \
|
||||
--hash=sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49 \
|
||||
--hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \
|
||||
--hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 \
|
||||
--hash=sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453 \
|
||||
--hash=sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf
|
||||
# via weasyprint
|
||||
cfgv==3.4.0 \
|
||||
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
|
||||
@@ -212,57 +109,73 @@ distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.18.0 \
|
||||
--hash=sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2 \
|
||||
--hash=sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
fonttools==4.59.0 \
|
||||
--hash=sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de \
|
||||
--hash=sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0 \
|
||||
--hash=sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d \
|
||||
--hash=sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df \
|
||||
--hash=sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d \
|
||||
--hash=sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe \
|
||||
--hash=sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64 \
|
||||
--hash=sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e \
|
||||
--hash=sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01 \
|
||||
--hash=sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705 \
|
||||
--hash=sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c \
|
||||
--hash=sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2 \
|
||||
--hash=sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b \
|
||||
--hash=sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f \
|
||||
--hash=sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97 \
|
||||
--hash=sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96 \
|
||||
--hash=sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2 \
|
||||
--hash=sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37 \
|
||||
--hash=sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64 \
|
||||
--hash=sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757 \
|
||||
--hash=sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e \
|
||||
--hash=sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3 \
|
||||
--hash=sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2 \
|
||||
--hash=sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c \
|
||||
--hash=sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0 \
|
||||
--hash=sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1 \
|
||||
--hash=sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e \
|
||||
--hash=sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c \
|
||||
--hash=sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea \
|
||||
--hash=sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5 \
|
||||
--hash=sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6 \
|
||||
--hash=sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c \
|
||||
--hash=sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d \
|
||||
--hash=sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db \
|
||||
--hash=sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14 \
|
||||
--hash=sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38 \
|
||||
--hash=sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e \
|
||||
--hash=sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482 \
|
||||
--hash=sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4 \
|
||||
--hash=sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b \
|
||||
--hash=sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464 \
|
||||
--hash=sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b
|
||||
fonttools==4.60.1 \
|
||||
--hash=sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c \
|
||||
--hash=sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc \
|
||||
--hash=sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a \
|
||||
--hash=sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856 \
|
||||
--hash=sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2 \
|
||||
--hash=sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259 \
|
||||
--hash=sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c \
|
||||
--hash=sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce \
|
||||
--hash=sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003 \
|
||||
--hash=sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272 \
|
||||
--hash=sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77 \
|
||||
--hash=sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038 \
|
||||
--hash=sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea \
|
||||
--hash=sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854 \
|
||||
--hash=sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2 \
|
||||
--hash=sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258 \
|
||||
--hash=sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652 \
|
||||
--hash=sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08 \
|
||||
--hash=sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99 \
|
||||
--hash=sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7 \
|
||||
--hash=sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914 \
|
||||
--hash=sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6 \
|
||||
--hash=sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed \
|
||||
--hash=sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb \
|
||||
--hash=sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217 \
|
||||
--hash=sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc \
|
||||
--hash=sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f \
|
||||
--hash=sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c \
|
||||
--hash=sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877 \
|
||||
--hash=sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801 \
|
||||
--hash=sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85 \
|
||||
--hash=sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a \
|
||||
--hash=sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb \
|
||||
--hash=sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383 \
|
||||
--hash=sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401 \
|
||||
--hash=sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28 \
|
||||
--hash=sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01 \
|
||||
--hash=sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036 \
|
||||
--hash=sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc \
|
||||
--hash=sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac \
|
||||
--hash=sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903 \
|
||||
--hash=sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3 \
|
||||
--hash=sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6 \
|
||||
--hash=sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c \
|
||||
--hash=sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da \
|
||||
--hash=sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299 \
|
||||
--hash=sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15 \
|
||||
--hash=sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199 \
|
||||
--hash=sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf \
|
||||
--hash=sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1 \
|
||||
--hash=sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537 \
|
||||
--hash=sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d \
|
||||
--hash=sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c \
|
||||
--hash=sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4 \
|
||||
--hash=sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9 \
|
||||
--hash=sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed \
|
||||
--hash=sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987 \
|
||||
--hash=sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa
|
||||
# via weasyprint
|
||||
identify==2.6.13 \
|
||||
--hash=sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b \
|
||||
--hash=sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
--hash=sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf
|
||||
# via pre-commit
|
||||
nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
@@ -338,128 +251,113 @@ pdf2image==1.17.0 \
|
||||
--hash=sha256:eaa959bc116b420dd7ec415fcae49b98100dda3dd18cd2fdfa86d09f112f6d57 \
|
||||
--hash=sha256:ecdd58d7afb810dffe21ef2b1bbc057ef434dabbac6c33778a38a3f7744a27e2
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pillow==11.3.0 \
|
||||
--hash=sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2 \
|
||||
--hash=sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214 \
|
||||
--hash=sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e \
|
||||
--hash=sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59 \
|
||||
--hash=sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50 \
|
||||
--hash=sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632 \
|
||||
--hash=sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06 \
|
||||
--hash=sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a \
|
||||
--hash=sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51 \
|
||||
--hash=sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced \
|
||||
--hash=sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f \
|
||||
--hash=sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12 \
|
||||
--hash=sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8 \
|
||||
--hash=sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6 \
|
||||
--hash=sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580 \
|
||||
--hash=sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f \
|
||||
--hash=sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac \
|
||||
--hash=sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860 \
|
||||
--hash=sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd \
|
||||
--hash=sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722 \
|
||||
--hash=sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8 \
|
||||
--hash=sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4 \
|
||||
--hash=sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673 \
|
||||
--hash=sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788 \
|
||||
--hash=sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542 \
|
||||
--hash=sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e \
|
||||
--hash=sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd \
|
||||
--hash=sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8 \
|
||||
--hash=sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523 \
|
||||
--hash=sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967 \
|
||||
--hash=sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809 \
|
||||
--hash=sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477 \
|
||||
--hash=sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027 \
|
||||
--hash=sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae \
|
||||
--hash=sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b \
|
||||
--hash=sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c \
|
||||
--hash=sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f \
|
||||
--hash=sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e \
|
||||
--hash=sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b \
|
||||
--hash=sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7 \
|
||||
--hash=sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27 \
|
||||
--hash=sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361 \
|
||||
--hash=sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae \
|
||||
--hash=sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d \
|
||||
--hash=sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc \
|
||||
--hash=sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58 \
|
||||
--hash=sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad \
|
||||
--hash=sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6 \
|
||||
--hash=sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024 \
|
||||
--hash=sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978 \
|
||||
--hash=sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb \
|
||||
--hash=sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d \
|
||||
--hash=sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0 \
|
||||
--hash=sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9 \
|
||||
--hash=sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f \
|
||||
--hash=sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874 \
|
||||
--hash=sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa \
|
||||
--hash=sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081 \
|
||||
--hash=sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149 \
|
||||
--hash=sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6 \
|
||||
--hash=sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d \
|
||||
--hash=sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd \
|
||||
--hash=sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f \
|
||||
--hash=sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c \
|
||||
--hash=sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31 \
|
||||
--hash=sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e \
|
||||
--hash=sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db \
|
||||
--hash=sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6 \
|
||||
--hash=sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f \
|
||||
--hash=sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494 \
|
||||
--hash=sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69 \
|
||||
--hash=sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94 \
|
||||
--hash=sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77 \
|
||||
--hash=sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d \
|
||||
--hash=sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7 \
|
||||
--hash=sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a \
|
||||
--hash=sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438 \
|
||||
--hash=sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288 \
|
||||
--hash=sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b \
|
||||
--hash=sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635 \
|
||||
--hash=sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3 \
|
||||
--hash=sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d \
|
||||
--hash=sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe \
|
||||
--hash=sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0 \
|
||||
--hash=sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe \
|
||||
--hash=sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a \
|
||||
--hash=sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805 \
|
||||
--hash=sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8 \
|
||||
--hash=sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36 \
|
||||
--hash=sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a \
|
||||
--hash=sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b \
|
||||
--hash=sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e \
|
||||
--hash=sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25 \
|
||||
--hash=sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12 \
|
||||
--hash=sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada \
|
||||
--hash=sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c \
|
||||
--hash=sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71 \
|
||||
--hash=sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d \
|
||||
--hash=sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c \
|
||||
--hash=sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6 \
|
||||
--hash=sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1 \
|
||||
--hash=sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50 \
|
||||
--hash=sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653 \
|
||||
--hash=sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c \
|
||||
--hash=sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4 \
|
||||
--hash=sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3
|
||||
pillow==12.0.0 \
|
||||
--hash=sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643 \
|
||||
--hash=sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e \
|
||||
--hash=sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e \
|
||||
--hash=sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc \
|
||||
--hash=sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642 \
|
||||
--hash=sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6 \
|
||||
--hash=sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1 \
|
||||
--hash=sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b \
|
||||
--hash=sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399 \
|
||||
--hash=sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba \
|
||||
--hash=sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad \
|
||||
--hash=sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47 \
|
||||
--hash=sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739 \
|
||||
--hash=sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b \
|
||||
--hash=sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f \
|
||||
--hash=sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10 \
|
||||
--hash=sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52 \
|
||||
--hash=sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d \
|
||||
--hash=sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b \
|
||||
--hash=sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a \
|
||||
--hash=sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9 \
|
||||
--hash=sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d \
|
||||
--hash=sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098 \
|
||||
--hash=sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905 \
|
||||
--hash=sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b \
|
||||
--hash=sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3 \
|
||||
--hash=sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371 \
|
||||
--hash=sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953 \
|
||||
--hash=sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01 \
|
||||
--hash=sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca \
|
||||
--hash=sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e \
|
||||
--hash=sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7 \
|
||||
--hash=sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27 \
|
||||
--hash=sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082 \
|
||||
--hash=sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e \
|
||||
--hash=sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d \
|
||||
--hash=sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8 \
|
||||
--hash=sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a \
|
||||
--hash=sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad \
|
||||
--hash=sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3 \
|
||||
--hash=sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a \
|
||||
--hash=sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d \
|
||||
--hash=sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353 \
|
||||
--hash=sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee \
|
||||
--hash=sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b \
|
||||
--hash=sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b \
|
||||
--hash=sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a \
|
||||
--hash=sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7 \
|
||||
--hash=sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef \
|
||||
--hash=sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a \
|
||||
--hash=sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a \
|
||||
--hash=sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257 \
|
||||
--hash=sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07 \
|
||||
--hash=sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4 \
|
||||
--hash=sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c \
|
||||
--hash=sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c \
|
||||
--hash=sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4 \
|
||||
--hash=sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe \
|
||||
--hash=sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8 \
|
||||
--hash=sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5 \
|
||||
--hash=sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6 \
|
||||
--hash=sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e \
|
||||
--hash=sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8 \
|
||||
--hash=sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e \
|
||||
--hash=sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275 \
|
||||
--hash=sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3 \
|
||||
--hash=sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76 \
|
||||
--hash=sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227 \
|
||||
--hash=sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9 \
|
||||
--hash=sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5 \
|
||||
--hash=sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79 \
|
||||
--hash=sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca \
|
||||
--hash=sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa \
|
||||
--hash=sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b \
|
||||
--hash=sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e \
|
||||
--hash=sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197 \
|
||||
--hash=sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab \
|
||||
--hash=sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79 \
|
||||
--hash=sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2 \
|
||||
--hash=sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363 \
|
||||
--hash=sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0 \
|
||||
--hash=sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e \
|
||||
--hash=sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782 \
|
||||
--hash=sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925 \
|
||||
--hash=sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0 \
|
||||
--hash=sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b \
|
||||
--hash=sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced \
|
||||
--hash=sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c \
|
||||
--hash=sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344 \
|
||||
--hash=sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9 \
|
||||
--hash=sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1
|
||||
# via
|
||||
# -r .github/scripts/requirements_dev.in
|
||||
# pdf2image
|
||||
# weasyprint
|
||||
platformdirs==4.3.8 \
|
||||
--hash=sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc \
|
||||
--hash=sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
pycparser==2.22 \
|
||||
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
|
||||
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
|
||||
pycparser==2.23 \
|
||||
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
|
||||
--hash=sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934
|
||||
# via cffi
|
||||
pydyf==0.11.0 \
|
||||
--hash=sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3 \
|
||||
@@ -469,60 +367,80 @@ pyphen==0.17.2 \
|
||||
--hash=sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd \
|
||||
--hash=sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3
|
||||
# via weasyprint
|
||||
pyyaml==6.0.2 \
|
||||
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
|
||||
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
|
||||
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
|
||||
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
|
||||
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
|
||||
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
|
||||
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
|
||||
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
|
||||
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
|
||||
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
|
||||
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
|
||||
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
|
||||
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
|
||||
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
|
||||
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
|
||||
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
|
||||
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
|
||||
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
|
||||
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
|
||||
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
|
||||
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
|
||||
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
|
||||
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
|
||||
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
|
||||
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
|
||||
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
|
||||
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
|
||||
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
|
||||
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
|
||||
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
|
||||
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
|
||||
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
|
||||
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
|
||||
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
|
||||
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
|
||||
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
|
||||
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
|
||||
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
|
||||
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
|
||||
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
|
||||
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
|
||||
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
|
||||
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
|
||||
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
|
||||
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
|
||||
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
|
||||
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
|
||||
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
|
||||
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
|
||||
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
|
||||
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
|
||||
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
|
||||
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
tinycss2==1.4.0 \
|
||||
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
|
||||
@@ -534,13 +452,13 @@ tinyhtml5==2.0.0 \
|
||||
--hash=sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc \
|
||||
--hash=sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e
|
||||
# via weasyprint
|
||||
unoserver==3.3.2 \
|
||||
--hash=sha256:1eeb7467cf6b56b8eff3b576e2d1b2b2ff4e0eb2052e995ac80a1456de300639 \
|
||||
--hash=sha256:87e144f903ee21951b2e06a97549450c13ed7eca5bcebad942d3352d4e882616
|
||||
unoserver==3.4 \
|
||||
--hash=sha256:3dcf2204013def1d1ddd3671f38b11346bdf349fef9728277462666a8a634419 \
|
||||
--hash=sha256:64c24d33d4f65d680a2d9f676518cb28e7fd6c1f9d9a745c33e4a4cb59afdfcd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
virtualenv==20.33.1 \
|
||||
--hash=sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67 \
|
||||
--hash=sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# via pre-commit
|
||||
weasyprint==66.0 \
|
||||
--hash=sha256:82b0783b726fcd318e2c977dcdddca76515b30044bc7a830cc4fbe717582a6d0 \
|
||||
@@ -628,9 +546,9 @@ zopfli==0.2.3.post1 \
|
||||
# via fonttools
|
||||
|
||||
# The following packages are considered to be unsafe in a requirements file:
|
||||
pip==25.2 \
|
||||
--hash=sha256:578283f006390f85bb6282dffb876454593d637f5d1be494b5202ce4877e71f2 \
|
||||
--hash=sha256:6d67a2b4e7f14d8b31b8b52648866fa717f45a1eb70e83002f4331d07e953717
|
||||
pip==25.3 \
|
||||
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
|
||||
--hash=sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd
|
||||
# via -r .github/scripts/requirements_dev.in
|
||||
setuptools==80.9.0 \
|
||||
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \
|
||||
|
||||
@@ -12,9 +12,9 @@ distlib==0.4.0 \
|
||||
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
|
||||
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
|
||||
# via virtualenv
|
||||
filelock==3.19.1 \
|
||||
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
|
||||
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
|
||||
filelock==3.20.0 \
|
||||
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
|
||||
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
|
||||
# via virtualenv
|
||||
identify==2.6.15 \
|
||||
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
|
||||
@@ -24,9 +24,9 @@ nodeenv==1.9.1 \
|
||||
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
|
||||
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
|
||||
# via pre-commit
|
||||
platformdirs==4.4.0 \
|
||||
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
|
||||
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
|
||||
platformdirs==4.5.0 \
|
||||
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
|
||||
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
|
||||
# via virtualenv
|
||||
pre-commit==4.3.0 \
|
||||
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
|
||||
@@ -107,7 +107,7 @@ pyyaml==6.0.3 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via pre-commit
|
||||
virtualenv==20.34.0 \
|
||||
--hash=sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026 \
|
||||
--hash=sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a
|
||||
virtualenv==20.35.4 \
|
||||
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
|
||||
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
|
||||
# via pre-commit
|
||||
|
||||
@@ -52,7 +52,6 @@ jobs:
|
||||
core.setOutput('repository', pr.head.repo.full_name);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
core.setOutput('is_fork', String(pr.head.repo.fork));
|
||||
core.setOutput('base_ref', pr.base.ref);
|
||||
core.setOutput('author', pr.user.login);
|
||||
core.setOutput('state', pr.state);
|
||||
|
||||
@@ -65,10 +64,6 @@ jobs:
|
||||
IS_FORK: ${{ steps.resolve.outputs.is_fork }}
|
||||
# nur bei workflow_dispatch gesetzt:
|
||||
ALLOW_FORK_INPUT: ${{ inputs.allow_fork }}
|
||||
# für Auto-PR-Logik:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BRANCH: ${{ github.event.pull_request.head.ref }}
|
||||
PR_BASE: ${{ steps.resolve.outputs.base_ref }}
|
||||
PR_AUTHOR: ${{ steps.resolve.outputs.author }}
|
||||
run: |
|
||||
set -e
|
||||
@@ -89,14 +84,8 @@ jobs:
|
||||
else
|
||||
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96" "balazs-szucs")
|
||||
is_auth=false; for u in "${auth_users[@]}"; do [ "$u" = "$PR_AUTHOR" ] && is_auth=true && break; done
|
||||
if [ "$PR_BASE" = "V2" ] && [ "$is_auth" = true ]; then
|
||||
if [ "$is_auth" = true ]; then
|
||||
should=true
|
||||
else
|
||||
title_has_v2=false; echo "$PR_TITLE" | grep -qiE 'v2|version.?2|version.?two' && title_has_v2=true
|
||||
branch_has_kw=false; echo "$PR_BRANCH" | grep -qiE 'v2|react' && branch_has_kw=true
|
||||
if [ "$is_auth" = true ] && { [ "$title_has_v2" = true ] || [ "$branch_has_kw" = true ]; }; then
|
||||
should=true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -174,7 +163,7 @@ jobs:
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment triggered by V2/version2 keywords in the PR title or V2/React keywords in the branch name._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||
body: `🚀 **Auto-deploying V2 version** for PR #${prNumber}...\n\n_This is an automated deployment for approved V2 contributors._\n\n⚠️ **Note:** If new commits are pushed during deployment, this build will be cancelled and replaced with the latest version.`
|
||||
});
|
||||
return newComment.id;
|
||||
|
||||
@@ -394,7 +383,7 @@ jobs:
|
||||
`🔗 **Direct Test URL (non-SSL)** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`🔐 **Secure HTTPS URL**: [${httpsUrl}](${httpsUrl})\n\n` +
|
||||
`_This deployment will be automatically cleaned up when the PR is closed._\n\n` +
|
||||
`🔄 **Auto-deployed** because PR title or branch name contains V2/version2/React keywords.`;
|
||||
`🔄 **Auto-deployed** for approved V2 contributors.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
|
||||
@@ -14,6 +14,7 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
if: |
|
||||
vars.CI_PROFILE != 'lite' &&
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
@@ -40,12 +41,12 @@ jobs:
|
||||
enable_enterprise: ${{ steps.check-pro-flag.outputs.enable_enterprise }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -128,12 +129,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -145,7 +146,7 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -180,7 +181,7 @@ jobs:
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=alpha
|
||||
@@ -356,3 +357,149 @@ jobs:
|
||||
rm -f ../private.key docker-compose.yml
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true
|
||||
|
||||
handle-label-commands:
|
||||
if: ${{ github.event.issue.pull_request != null }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Apply label commands
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { comment, issue } = context.payload;
|
||||
const commentBody = comment?.body ?? '';
|
||||
if (!commentBody.includes('::label::')) {
|
||||
core.info('No label commands detected in comment.');
|
||||
return;
|
||||
}
|
||||
|
||||
const configPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'config', 'repo_devs.json');
|
||||
const repoDevsConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const label_changer = (repoDevsConfig.label_changer || []).map((login) => login.toLowerCase());
|
||||
|
||||
const commenter = (comment?.user?.login || '').toLowerCase();
|
||||
if (!label_changer.includes(commenter)) {
|
||||
core.info(`User ${commenter} is not authorized to manage labels.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labelsConfigPath = path.join(process.env.GITHUB_WORKSPACE, '.github', 'labels.yml');
|
||||
const labelsFile = fs.readFileSync(labelsConfigPath, 'utf8');
|
||||
|
||||
const labelNameMap = new Map();
|
||||
for (const match of labelsFile.matchAll(/-\s+name:\s*(?:"([^"]+)"|'([^']+)'|([^\n]+))/g)) {
|
||||
const labelName = (match[1] ?? match[2] ?? match[3] ?? '').trim();
|
||||
|
||||
if (!labelName) {
|
||||
continue;
|
||||
}
|
||||
const normalized = labelName.toLowerCase();
|
||||
if (!labelNameMap.has(normalized)) {
|
||||
labelNameMap.set(normalized, labelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!labelNameMap.size) {
|
||||
core.warning('No labels could be read from .github/labels.yml; aborting label commands.');
|
||||
return;
|
||||
}
|
||||
|
||||
let allowedLabelNames = new Set(labelNameMap.values());
|
||||
|
||||
const labelsToAdd = new Set();
|
||||
const labelsToRemove = new Set();
|
||||
const commandRegex = /^(\w+)::(label)::"([^"]+)"/gim;
|
||||
let match;
|
||||
while ((match = commandRegex.exec(commentBody)) !== null) {
|
||||
core.info(`Found label command: ${match[0]} (action: ${match[1]}, label: ${match[2]}, labelName: ${match[3]})`);
|
||||
const action = match[1].toLowerCase();
|
||||
const labelName = match[3].trim();
|
||||
|
||||
if (!labelName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalized = labelName.toLowerCase();
|
||||
const resolvedLabelName = labelNameMap.get(normalized);
|
||||
if (action === 'add') {
|
||||
if (!resolvedLabelName) {
|
||||
core.warning(`Label "${labelName}" is not defined in .github/labels.yml and cannot be added.`);
|
||||
continue;
|
||||
}
|
||||
if (!allowedLabelNames.has(resolvedLabelName)) {
|
||||
core.warning(`Label "${resolvedLabelName}" is not allowed for add commands and will be skipped.`);
|
||||
continue;
|
||||
}
|
||||
labelsToAdd.add(resolvedLabelName);
|
||||
} else if (action === 'rm') {
|
||||
const labelToRemove = resolvedLabelName ?? labelName;
|
||||
if (!resolvedLabelName) {
|
||||
core.warning(`Label "${labelName}" is not defined in .github/labels.yml; attempting to remove as provided.`);
|
||||
}
|
||||
labelsToRemove.add(labelToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
const addLabels = Array.from(labelsToAdd);
|
||||
const removeLabels = Array.from(labelsToRemove);
|
||||
|
||||
if (!addLabels.length && !removeLabels.length) {
|
||||
core.info('No valid label commands found after parsing.');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueParams = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issue.number,
|
||||
};
|
||||
|
||||
if (addLabels.length) {
|
||||
core.info(`Adding labels: ${addLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
...issueParams,
|
||||
labels: addLabels,
|
||||
});
|
||||
}
|
||||
|
||||
for (const labelName of removeLabels) {
|
||||
core.info(`Removing label: ${labelName}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
...issueParams,
|
||||
name: labelName,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
core.warning(`Label "${labelName}" was not present on the pull request.`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: comment.id,
|
||||
});
|
||||
core.info('Processed label commands and deleted the comment.');
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
|
||||
@@ -19,11 +19,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ name: "Auto Pull Request Labeler V2"
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
branches:
|
||||
- main
|
||||
- V2
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -13,11 +16,11 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
|
||||
@@ -262,7 +262,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
docker-rev: ["Dockerfile", "Dockerfile.ultra-lite", "Dockerfile.fat"]
|
||||
include:
|
||||
- docker-rev: docker/embedded/Dockerfile
|
||||
artifact-suffix: Dockerfile
|
||||
- docker-rev: docker/embedded/Dockerfile.ultra-lite
|
||||
artifact-suffix: Dockerfile.ultra-lite
|
||||
- docker-rev: docker/embedded/Dockerfile.fat
|
||||
artifact-suffix: Dockerfile.fat
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
@@ -272,6 +278,13 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Free disk space on runner
|
||||
run: |
|
||||
echo "Disk space before cleanup:" && df -h
|
||||
sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android /usr/local/share/boost
|
||||
docker system prune -af || true
|
||||
echo "Disk space after cleanup:" && df -h
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
@@ -301,7 +314,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/backend/${{ matrix.docker-rev }}
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -313,7 +326,7 @@ jobs:
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: reports-docker-${{ matrix.docker-rev }}
|
||||
name: reports-docker-${{ matrix.artifact-suffix }}
|
||||
path: |
|
||||
build/reports/tests/
|
||||
build/test-results/
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
name: Check Properties Files on PR
|
||||
name: Check TOML Translation Files on PR
|
||||
|
||||
# This workflow validates TOML translation files
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "app/core/src/main/resources/messages_*.properties"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
@@ -30,12 +25,12 @@ jobs:
|
||||
pull-requests: write # Allow writing to pull requests
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -73,22 +68,22 @@ jobs:
|
||||
run: |
|
||||
echo "Fetching PR changed files..."
|
||||
echo "Getting list of changed files from PR..."
|
||||
# Check if PR number exists
|
||||
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
|
||||
echo "Error: PR number is empty"
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for properties files, handle case where no matches are found
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^app/core/src/main/resources/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$' > changed_files.txt || echo "No matching properties files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No properties files changed in this PR"
|
||||
echo "Workflow will exit early as no relevant files to check"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found $(wc -l < changed_files.txt) matching properties files"
|
||||
# Check if PR number exists
|
||||
if [ -z "${{ steps.get-pr-data.outputs.pr_number }}" ]; then
|
||||
echo "Error: PR number is empty"
|
||||
exit 1
|
||||
fi
|
||||
# Get changed files and filter for TOML translation files
|
||||
gh pr view ${{ steps.get-pr-data.outputs.pr_number }} --json files -q ".files[].path" | grep -E '^frontend/public/locales/[a-zA-Z-]+/translation\.toml$' > changed_files.txt || echo "No matching TOML files found in PR"
|
||||
# Check if any files were found
|
||||
if [ ! -s changed_files.txt ]; then
|
||||
echo "No TOML translation files changed in this PR"
|
||||
echo "Workflow will exit early as no relevant files to check"
|
||||
exit 0
|
||||
fi
|
||||
echo "Found $(wc -l < changed_files.txt) matching TOML files"
|
||||
|
||||
- name: Determine reference file test
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
@@ -125,11 +120,11 @@ jobs:
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// Filter for relevant files based on the PR changes
|
||||
// Filter for relevant TOML files based on the PR changes
|
||||
const changedFiles = files
|
||||
.filter(file =>
|
||||
file.status !== "removed" &&
|
||||
/^app\/core\/src\/main\/resources\/messages_[a-zA-Z_]{2}_[a-zA-Z_]{2,7}\.properties$/.test(file.filename)
|
||||
/^frontend\/public\/locales\/[a-zA-Z-]+\/translation\.toml$/.test(file.filename)
|
||||
)
|
||||
.map(file => file.filename);
|
||||
|
||||
@@ -169,16 +164,16 @@ jobs:
|
||||
|
||||
// Determine reference file
|
||||
let referenceFilePath;
|
||||
if (changedFiles.includes("app/core/src/main/resources/messages_en_GB.properties")) {
|
||||
if (changedFiles.includes("frontend/public/locales/en-GB/translation.toml")) {
|
||||
console.log("Using PR branch reference file.");
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: prRepoOwner,
|
||||
repo: prRepoName,
|
||||
path: "app/core/src/main/resources/messages_en_GB.properties",
|
||||
path: "frontend/public/locales/en-GB/translation.toml",
|
||||
ref: branch,
|
||||
});
|
||||
|
||||
referenceFilePath = "pr-branch-messages_en_GB.properties";
|
||||
referenceFilePath = "pr-branch-translation-en-GB.toml";
|
||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||
fs.writeFileSync(referenceFilePath, content);
|
||||
} else {
|
||||
@@ -186,11 +181,11 @@ jobs:
|
||||
const { data: fileContent } = await github.rest.repos.getContent({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
path: "app/core/src/main/resources/messages_en_GB.properties",
|
||||
path: "frontend/public/locales/en-GB/translation.toml",
|
||||
ref: "main",
|
||||
});
|
||||
|
||||
referenceFilePath = "main-branch-messages_en_GB.properties";
|
||||
referenceFilePath = "main-branch-translation-en-GB.toml";
|
||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||
fs.writeFileSync(referenceFilePath, content);
|
||||
}
|
||||
@@ -198,11 +193,20 @@ jobs:
|
||||
console.log(`Reference file path: ${referenceFilePath}`);
|
||||
core.exportVariable("REFERENCE_FILE", referenceFilePath);
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
pip install tomli-w
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
echo "Running Python script to check files..."
|
||||
python .github/scripts/check_language_properties.py \
|
||||
echo "Running Python script to check TOML files..."
|
||||
python .github/scripts/check_language_toml.py \
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch "pr-branch" \
|
||||
@@ -213,7 +217,7 @@ jobs:
|
||||
id: capture-output
|
||||
run: |
|
||||
if [ -f result.txt ] && [ -s result.txt ]; then
|
||||
echo "Test, capturing output..."
|
||||
echo "Capturing output..."
|
||||
SCRIPT_OUTPUT=$(cat result.txt)
|
||||
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
|
||||
@@ -227,7 +231,7 @@ jobs:
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
fi
|
||||
else
|
||||
echo "No update found."
|
||||
echo "No output found."
|
||||
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
fi
|
||||
@@ -249,7 +253,7 @@ jobs:
|
||||
issue_number: issueNumber
|
||||
});
|
||||
|
||||
const comment = comments.data.find(c => c.body.includes("## 🚀 Translation Verification Summary"));
|
||||
const comment = comments.data.find(c => c.body.includes("## 🌐 TOML Translation Verification Summary"));
|
||||
|
||||
// Only update or create comments by the action user
|
||||
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||
@@ -260,7 +264,7 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
comment_id: comment.id,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
});
|
||||
console.log("Updated existing comment.");
|
||||
} else if (!comment) {
|
||||
@@ -269,7 +273,7 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: issueNumber,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
body: `## 🌐 TOML Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
});
|
||||
console.log("Created new comment.");
|
||||
} else {
|
||||
@@ -287,6 +291,6 @@ jobs:
|
||||
run: |
|
||||
echo "Cleaning up temporary files..."
|
||||
rm -rf pr-branch
|
||||
rm -f pr-branch-messages_en_GB.properties main-branch-messages_en_GB.properties changed_files.txt result.txt
|
||||
rm -f pr-branch-translation-en-GB.toml main-branch-translation-en-GB.toml changed_files.txt result.txt
|
||||
echo "Cleanup complete."
|
||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||
@@ -17,13 +17,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
|
||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
||||
with:
|
||||
config-file: './.github/config/dependency-review-config.yml'
|
||||
|
||||
@@ -31,12 +31,12 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
- name: Upload artifact on failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
if: env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update 3rd Party Licenses"
|
||||
|
||||
@@ -15,12 +15,12 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Run Labeler
|
||||
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
|
||||
|
||||
@@ -31,20 +31,21 @@ permissions:
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
@@ -105,14 +106,14 @@ jobs:
|
||||
file_suffix: "-server"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
@@ -144,7 +145,7 @@ jobs:
|
||||
cp app/core/build/libs/stirling-pdf-${{ needs.determine-matrix.outputs.version }}.jar ./jar-dist/Stirling-PDF${{ matrix.variant.file_suffix }}.jar
|
||||
|
||||
- name: Upload JAR artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: jar${{ matrix.variant.file_suffix }}
|
||||
path: ./jar-dist/*.jar
|
||||
@@ -188,7 +189,7 @@ jobs:
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
@@ -515,7 +516,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
path: ./dist/*
|
||||
@@ -529,30 +530,30 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download all Tauri artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: Stirling-PDF-*
|
||||
path: ./artifacts/tauri
|
||||
|
||||
- name: Download JAR artifact (default)
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (with login)
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar-with-login
|
||||
path: ./artifacts/jars
|
||||
|
||||
- name: Download JAR artifact (server only)
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: jar-server
|
||||
path: ./artifacts/jars
|
||||
@@ -561,7 +562,7 @@ jobs:
|
||||
run: ls -R ./artifacts
|
||||
|
||||
- name: Upload binaries to Release
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: ":file_folder: pre-commit"
|
||||
|
||||
@@ -5,6 +5,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- V2-master
|
||||
- V1_V2_merge
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
@@ -23,6 +24,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-24.04-8core
|
||||
permissions:
|
||||
packages: write
|
||||
@@ -93,10 +95,10 @@ jobs:
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
type=raw,value=latest
|
||||
|
||||
- name: Generate tags for latest (V2-demo branch - test)
|
||||
- name: Generate tags for latest (V1_V2_merge branch - test)
|
||||
id: meta-test
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
if: github.ref == 'refs/heads/V2-demo'
|
||||
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stirling-tools/stirling-pdf-test
|
||||
@@ -110,7 +112,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile.unified
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -149,10 +151,10 @@ jobs:
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat
|
||||
type=raw,value=latest-fat
|
||||
|
||||
- name: Generate tags for latest-fat (V2-demo branch - test)
|
||||
- name: Generate tags for latest-fat (V1_V2_merge branch - test)
|
||||
id: meta-fat-test
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
if: github.ref == 'refs/heads/V2-demo'
|
||||
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stirling-tools/stirling-pdf-test
|
||||
@@ -166,7 +168,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile.unified
|
||||
file: ./docker/embedded/Dockerfile.fat
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -203,10 +205,10 @@ jobs:
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite
|
||||
type=raw,value=latest-ultra-lite
|
||||
|
||||
- name: Generate tags for ultra-lite (V2-demo branch - test)
|
||||
- name: Generate tags for ultra-lite (V1_V2_merge branch - test)
|
||||
id: meta-lite-test
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
if: github.ref == 'refs/heads/V2-demo'
|
||||
if: github.ref == 'refs/heads/V1_V2_merge'
|
||||
with:
|
||||
images: |
|
||||
ghcr.io/stirling-tools/stirling-pdf-test
|
||||
@@ -220,7 +222,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./docker/Dockerfile.unified-lite
|
||||
file: ./docker/embedded/Dockerfile.ultra-lite
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -24,17 +24,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -54,7 +55,7 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
@@ -80,7 +81,7 @@ jobs:
|
||||
password: ${{ github.token }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
|
||||
- name: Convert repository owner to lowercase
|
||||
id: repoowner
|
||||
@@ -88,7 +89,7 @@ jobs:
|
||||
|
||||
- name: Generate tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
@@ -107,7 +108,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -134,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Generate tags ultra-lite
|
||||
id: meta2
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
images: |
|
||||
@@ -152,7 +153,7 @@ jobs:
|
||||
if: github.ref != 'refs/heads/main'
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.ultra-lite
|
||||
file: ./docker/embedded/Dockerfile.ultra-lite
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -165,7 +166,7 @@ jobs:
|
||||
|
||||
- name: Generate tags fat
|
||||
id: meta3
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
@@ -183,7 +184,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./Dockerfile.fat
|
||||
file: ./docker/embedded/Dockerfile.fat
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
version: ${{ steps.versionNumber.outputs.versionNumber }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
@@ -68,12 +68,12 @@ jobs:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
targets: ${{ (matrix.platform == 'macos-15' || matrix.platform == 'macos-15-intel') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
|
||||
@@ -517,7 +517,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Download all signed artifacts
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
with:
|
||||
pattern: Stirling-PDF-*-signed
|
||||
path: ./artifacts
|
||||
@@ -526,7 +526,7 @@ jobs:
|
||||
run: ls -R ./artifacts
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
|
||||
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -17,6 +17,7 @@ permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -34,12 +35,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: "Checkout code"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -66,7 +67,7 @@ jobs:
|
||||
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
|
||||
# format to the repository Actions tab.
|
||||
- name: "Upload artifact"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
@@ -74,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Run Sonarqube
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
pull-requests: read
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
sonarqube:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
|
||||
- name: Build and analyze with Gradle
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
run: |
|
||||
./gradlew clean build sonar \
|
||||
-Dsonar.projectKey=Stirling-Tools_Stirling-PDF \
|
||||
-Dsonar.organization=stirling-tools \
|
||||
-Dsonar.host.url=https://sonarcloud.io \
|
||||
-Dsonar.login=${SONAR_TOKEN} \
|
||||
-Dsonar.log.level=DEBUG \
|
||||
--info
|
||||
|
||||
- name: Upload Problems Report on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: gradle-problems-report
|
||||
path: build/reports/problems/problems-report.html
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Sonar Logs on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: sonar-logs
|
||||
path: |
|
||||
.scannerwork/report-task.txt
|
||||
build/sonar/
|
||||
retention-days: 7
|
||||
@@ -10,13 +10,14 @@ permissions:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -23,14 +23,15 @@ permissions:
|
||||
|
||||
jobs:
|
||||
push:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
name: Sync Files
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "app/core/src/main/resources/messages_*.properties"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-files:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# Prevents sdist builds → no tar extraction
|
||||
PIP_ONLY_BINARY: ":all:"
|
||||
PIP_DISABLE_PIP_VERSION_CHECK: "1"
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Sync translation property files
|
||||
run: |
|
||||
python .github/scripts/check_language_properties.py --reference-file "app/core/src/main/resources/messages_en_GB.properties" --branch main
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add app/core/src/main/resources/messages_*.properties
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
# Wheels-only + Hash-Pinning
|
||||
run: |
|
||||
pip install --require-hashes --only-binary=:all: -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
git add README.md scripts/ignore_translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: Update files
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: sync_readme
|
||||
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
|
||||
body: |
|
||||
### Description of Changes
|
||||
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`messages_*.properties`) to reflect changes in the reference file `messages_en_GB.properties`.
|
||||
- Ensured consistency and synchronization across all supported language files.
|
||||
- Highlighted any missing or incomplete translations.
|
||||
|
||||
#### **2. Update README.md**
|
||||
- Generated the translation progress table in `README.md`.
|
||||
- Added a summary of the current translation status for all supported languages.
|
||||
- Included up-to-date statistics on translation coverage.
|
||||
|
||||
#### **Why these changes are necessary**
|
||||
- Keeps translation files aligned with the latest reference updates.
|
||||
- Ensures the documentation reflects the current translation progress.
|
||||
|
||||
---
|
||||
|
||||
Auto-generated by [create-pull-request][1].
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
draft: false
|
||||
delete-branch: true
|
||||
labels: github-actions
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
app/core/src/main/resources/messages_*.properties
|
||||
@@ -1,15 +1,15 @@
|
||||
name: Sync Files V2
|
||||
name: Sync Files (TOML)
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- main
|
||||
- syncLangTest
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.json"
|
||||
- "frontend/public/locales/*/translation.toml"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
@@ -33,11 +33,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
@@ -47,26 +47,30 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Sync translation JSON files
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python .github/scripts/check_language_json.py --reference-file "frontend/public/locales/en-GB/translation.json" --branch V2
|
||||
pip install tomli-w
|
||||
|
||||
- name: Sync translation TOML files
|
||||
run: |
|
||||
python .github/scripts/check_language_toml.py --reference-file "frontend/public/locales/en-GB/translation.toml" --branch main
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add frontend/public/locales/*/translation.json
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
git add frontend/public/locales/*/translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files (TOML)" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install README dependencies
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation_v2.py
|
||||
python scripts/counter_translation_v3.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
@@ -82,21 +86,22 @@ jobs:
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: sync_readme_v2
|
||||
base: V2
|
||||
title: ":globe_with_meridians: [V2] Sync Translations + Update README Progress Table"
|
||||
branch: sync_readme_v3
|
||||
base: main
|
||||
title: ":globe_with_meridians: Sync Translations + Update README Progress Table"
|
||||
body: |
|
||||
### Description of Changes
|
||||
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation for the **V2 branch**. Below are the details of the changes made:
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`.
|
||||
- Updated translation files (`frontend/public/locales/*/translation.toml`) to reflect changes in the reference file `en-GB/translation.toml`.
|
||||
- Ensured consistency and synchronization across all supported language files.
|
||||
- Highlighted any missing or incomplete translations.
|
||||
- **Format**: TOML
|
||||
|
||||
#### **2. Update README.md**
|
||||
- Generated the translation progress table in `README.md`.
|
||||
- Generated the translation progress table in `README.md` using `counter_translation_v3.py`.
|
||||
- Added a summary of the current translation status for all supported languages.
|
||||
- Included up-to-date statistics on translation coverage.
|
||||
|
||||
@@ -115,4 +120,5 @@ jobs:
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
frontend/public/locales/*/translation.json
|
||||
frontend/public/locales/*/translation.toml
|
||||
scripts/ignore_translation.toml
|
||||
@@ -28,6 +28,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
@@ -636,6 +637,8 @@ jobs:
|
||||
if [ "${{ needs.build.result }}" = "success" ]; then
|
||||
echo "✅ All Tauri builds completed successfully!"
|
||||
echo "Artifacts are ready for distribution."
|
||||
elif [ "${{ needs.build.result }}" = "skipped" ]; then
|
||||
echo "⏭️ Tauri builds skipped (CI lite mode enabled)"
|
||||
else
|
||||
echo "❌ Some Tauri builds failed."
|
||||
echo "Please check the logs and fix any issues."
|
||||
|
||||
@@ -21,15 +21,16 @@ permissions:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: ${{ vars.CI_PROFILE != 'lite' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
@@ -66,7 +67,7 @@ jobs:
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
file: ./docker/embedded/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:test-${{ github.sha }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
@@ -124,7 +125,7 @@ jobs:
|
||||
outputs:
|
||||
frontend: ${{ steps.changes.outputs.frontend }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
@@ -139,14 +140,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
@@ -175,7 +176,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.7
|
||||
rev: v0.14.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
@@ -26,7 +26,7 @@ repos:
|
||||
hooks:
|
||||
- id: gitleaks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: end-of-file-fixer
|
||||
files: ^.*(\.js|\.java|\.py|\.yml)$
|
||||
|
||||
+3
-3
@@ -202,10 +202,10 @@ const [ToolName] = (props: BaseToolProps) => {
|
||||
## 5. Add Translations
|
||||
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
|
||||
|
||||
**File to update:** `frontend/public/locales/en-GB/translation.json`
|
||||
**File to update:** `frontend/public/locales/en-GB/translation.toml`
|
||||
|
||||
**Required Translation Keys**:
|
||||
```json
|
||||
```toml
|
||||
{
|
||||
"home": {
|
||||
"[toolName]": {
|
||||
@@ -251,7 +251,7 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
|
||||
```
|
||||
|
||||
**Translation Notes:**
|
||||
- **Only update `en-GB/translation.json`** - other locale files are managed separately
|
||||
- **Only update `en-GB/translation.toml`** - other locale files are managed separately
|
||||
- Use descriptive keys that match your component's `t()` calls
|
||||
- Include tooltip translations if you created tooltip hooks
|
||||
- Add `options.*` keys if your tool has settings with descriptions
|
||||
|
||||
@@ -1,173 +1,69 @@
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80"></p>
|
||||
<h1 align="center">Stirling-PDF</h1>
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" alt="Stirling PDF logo">
|
||||
</p>
|
||||
|
||||
[](https://hub.docker.com/r/frooodle/s-pdf)
|
||||
[](https://discord.gg/HYmhKj45pU)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF)
|
||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||
<h1 align="center">Stirling PDF - The Open-Source PDF Platform</h1>
|
||||
|
||||
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
Stirling PDF is a powerful, open-source PDF editing platform. Run it as a personal desktop app, in the browser, or deploy it on your own servers with a private API. Edit, sign, redact, convert, and automate PDFs without sending documents to external services.
|
||||
|
||||
[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.
|
||||
<p align="center">
|
||||
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf">
|
||||
<img src="https://img.shields.io/docker/pulls/frooodle/s-pdf" alt="Docker Pulls">
|
||||
</a>
|
||||
<a href="https://discord.gg/HYmhKj45pU">
|
||||
<img src="https://img.shields.io/discord/1068636748814483718?label=Discord" alt="Discord">
|
||||
</a>
|
||||
<a href="https://scorecard.dev/viewer/?uri=github.com/Stirling-Tools/Stirling-PDF">
|
||||
<img src="https://api.scorecard.dev/projects/github.com/Stirling-Tools/Stirling-PDF/badge" alt="OpenSSF Scorecard">
|
||||
</a>
|
||||
<a href="https://github.com/Stirling-Tools/stirling-pdf">
|
||||
<img src="https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social" alt="GitHub Repo stars">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
|
||||

|
||||
|
||||
Homepage: [https://stirlingpdf.com](https://stirlingpdf.com)
|
||||
## Key Capabilities
|
||||
|
||||
All documentation available at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/)
|
||||
- **Everywhere you work** - Desktop client, browser UI, and self-hosted server with a private API.
|
||||
- **50+ PDF tools** - Edit, merge, split, sign, redact, convert, OCR, compress, and more.
|
||||
- **Automation & workflows** - No-code pipelines direct in UI with APIs to process millions of PDFs.
|
||||
- **Enterprise‑grade** - SSO, auditing, and flexible on‑prem deployments.
|
||||
- **Developer platform** - REST APIs available for nearly all tools to integrate into your existing systems.
|
||||
- **Global UI** - Interface available in 40+ languages.
|
||||
|
||||

|
||||
For a full feature list, see the docs: **https://docs.stirlingpdf.com**
|
||||
|
||||
## Features
|
||||
## Quick Start
|
||||
|
||||
- 50+ PDF Operations
|
||||
- Parallel file processing and downloads
|
||||
- Dark mode support
|
||||
- Custom download options
|
||||
- Custom 'Pipelines' to run multiple features in a automated queue
|
||||
- API for integration with external scripts
|
||||
- Optional Login and Authentication support (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/System%20and%20Security) for documentation)
|
||||
- Database Backup and Import (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/DATABASE) for documentation)
|
||||
- Enterprise features like SSO (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration) for documentation)
|
||||
```bash
|
||||
docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf
|
||||
```
|
||||
|
||||
## PDF Features
|
||||
Then open: http://localhost:8080
|
||||
|
||||
### Page Operations
|
||||
For full installation options (including desktop and Kubernetes), see our [Documentation Guide](https://docs.stirlingpdf.com/#documentation-guide).
|
||||
|
||||
- View and modify PDFs - View multi-page PDFs with custom viewing, sorting, and searching. Plus, on-page edit features like annotating, drawing, and adding text and images. (Using PDF.js with Joxit and Liberation fonts)
|
||||
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages
|
||||
- Merge multiple PDFs into a single resultant file
|
||||
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files
|
||||
- Reorganize PDF pages into different orders
|
||||
- Rotate PDFs in 90-degree increments
|
||||
- Remove pages
|
||||
- Multi-page layout (format PDFs into a multi-paged page)
|
||||
- Scale page contents size by set percentage
|
||||
- Adjust contrast
|
||||
- Crop PDF
|
||||
- Auto-split PDF (with physically scanned page dividers)
|
||||
- Extract page(s)
|
||||
- Convert PDF to a single page
|
||||
- Overlay PDFs on top of each other
|
||||
- PDF to a single page
|
||||
- Split PDF by sections
|
||||
## Resources
|
||||
|
||||
### Conversion Operations
|
||||
- [**Documentation**](https://docs.stirlingpdf.com)
|
||||
- [**Homepage**](https://stirling.com)
|
||||
- [**API Docs**](https://registry.scalar.com/@stirlingpdf/apis/stirling-pdf-processing-api/)
|
||||
- [**Server Plan & Enterprise**](https://docs.stirlingpdf.com/Paid-Offerings)
|
||||
|
||||
- Convert PDFs to and from images
|
||||
- Convert any common file to PDF (using LibreOffice)
|
||||
- Convert PDF to Word/PowerPoint/others (using LibreOffice)
|
||||
- Convert HTML to PDF
|
||||
- Convert PDF to XML
|
||||
- Convert PDF to CSV
|
||||
- URL to PDF
|
||||
- Markdown to PDF
|
||||
## Support
|
||||
|
||||
### Security & Permissions
|
||||
- **Community** [Discord](https://discord.gg/HYmhKj45pU)
|
||||
- **Bug Reports**: [Github issues](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
|
||||
- Add and remove passwords
|
||||
- Change/set PDF permissions
|
||||
- Add watermark(s)
|
||||
- Certify/sign PDFs
|
||||
- Sanitize PDFs
|
||||
- Auto-redact text
|
||||
## Contributing
|
||||
|
||||
### Other Operations
|
||||
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
- Add/generate/write signatures
|
||||
- Split by Size or PDF
|
||||
- Repair PDFs
|
||||
- Detect and remove blank pages
|
||||
- Compare two PDFs and show differences in text
|
||||
- Add images to PDFs
|
||||
- Compress PDFs to decrease their filesize (using qpdf)
|
||||
- Extract images from PDF
|
||||
- Remove images from PDF
|
||||
- Extract images from scans
|
||||
- Remove annotations
|
||||
- Add page numbers
|
||||
- Auto-rename files by detecting PDF header text
|
||||
- OCR on PDF (using Tesseract OCR)
|
||||
- PDF/A conversion (using LibreOffice)
|
||||
- Edit metadata
|
||||
- Flatten PDFs
|
||||
- Get all information on a PDF to view or export as JSON
|
||||
- Show/detect embedded JavaScript
|
||||
For development setup, see the [Developer Guide](DeveloperGuide.md).
|
||||
|
||||
For adding translations, see the [Translation Guide](devGuide/HowToAddNewLanguage.md).
|
||||
|
||||
## License
|
||||
|
||||
# 📖 Get Started
|
||||
|
||||
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for:
|
||||
|
||||
- Installation guides for all platforms
|
||||
- Configuration options
|
||||
- Feature documentation
|
||||
- API reference
|
||||
- Security setup
|
||||
- Enterprise features
|
||||
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
Stirling PDF offers an Enterprise edition of its software. This is the same great software but with added features, support and comforts.
|
||||
Check out our [Enterprise docs](https://docs.stirlingpdf.com/Pro)
|
||||
|
||||
|
||||
## 🤝 Looking to contribute?
|
||||
|
||||
Join our community:
|
||||
- [Contribution Guidelines](CONTRIBUTING.md)
|
||||
- [Translation Guide (How to add custom languages)](devGuide/HowToAddNewLanguage.md)
|
||||
- [Developer Guide](devGuide/DeveloperGuide.md)
|
||||
- [Issue Tracker](https://github.com/Stirling-Tools/Stirling-PDF/issues)
|
||||
- [Discord Community](https://discord.gg/HYmhKj45pU)
|
||||
Stirling PDF is open-core. See [LICENSE](LICENSE) for details.
|
||||
|
||||
@@ -33,16 +33,16 @@ dependencies {
|
||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||
api 'com.fathzer:javaluator:3.0.6'
|
||||
api 'com.posthog.java:posthog:1.2.0'
|
||||
api 'org.apache.commons:commons-lang3:3.19.0'
|
||||
api 'org.apache.commons:commons-lang3:3.20.0'
|
||||
api 'com.drewnoakes:metadata-extractor:2.19.0' // Image metadata extractor
|
||||
api 'com.vladsch.flexmark:flexmark-html2md-converter:0.64.8'
|
||||
api "org.apache.pdfbox:pdfbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:xmpbox:$pdfboxVersion"
|
||||
api "org.apache.pdfbox:preflight:$pdfboxVersion"
|
||||
api 'com.github.junrar:junrar:7.5.5' // RAR archive support for CBR files
|
||||
api 'com.github.junrar:junrar:7.5.7' // RAR archive support for CBR files
|
||||
api 'jakarta.servlet:jakarta.servlet-api:6.1.0'
|
||||
api 'org.snakeyaml:snakeyaml-engine:2.10'
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13"
|
||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.14"
|
||||
api 'jakarta.mail:jakarta.mail-api:2.1.5'
|
||||
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.5'
|
||||
}
|
||||
|
||||
@@ -334,6 +334,9 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Convert", "pdf-to-csv");
|
||||
addEndpointToGroup("Convert", "pdf-to-markdown");
|
||||
addEndpointToGroup("Convert", "eml-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-vector");
|
||||
addEndpointToGroup("Convert", "vector-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-video");
|
||||
addEndpointToGroup("Convert", "cbz-to-pdf");
|
||||
addEndpointToGroup("Convert", "pdf-to-cbz");
|
||||
addEndpointToGroup("Convert", "pdf-to-json");
|
||||
@@ -490,6 +493,13 @@ public class EndpointConfiguration {
|
||||
/* Ghostscript */
|
||||
addEndpointToGroup("Ghostscript", "repair");
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "crop");
|
||||
addEndpointToGroup("Ghostscript", "replace-invert-pdf");
|
||||
addEndpointToGroup("Ghostscript", "pdf-to-vector");
|
||||
addEndpointToGroup("Ghostscript", "vector-to-pdf");
|
||||
|
||||
/* ImageMagick */
|
||||
addEndpointToGroup("ImageMagick", "compress-pdf");
|
||||
|
||||
/* tesseract */
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
@@ -551,7 +561,7 @@ public class EndpointConfiguration {
|
||||
disableGroup("enterprise");
|
||||
}
|
||||
|
||||
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
|
||||
if (!applicationProperties.getSystem().isEnableUrlToPDF()) {
|
||||
disableEndpoint("url-to-pdf");
|
||||
}
|
||||
}
|
||||
@@ -574,6 +584,7 @@ public class EndpointConfiguration {
|
||||
|| "Javascript".equals(group)
|
||||
|| "Weasyprint".equals(group)
|
||||
|| "Pdftohtml".equals(group)
|
||||
|| "ImageMagick".equals(group)
|
||||
|| "rar".equals(group);
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public class AutoJobAspect {
|
||||
|
||||
@Around("@annotation(autoJobPostMapping)")
|
||||
public Object wrapWithJobExecution(
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) {
|
||||
ProceedingJoinPoint joinPoint, AutoJobPostMapping autoJobPostMapping) throws Exception {
|
||||
// This aspect will run before any audit aspects due to @Order(0)
|
||||
// Extract parameters from the request and annotation
|
||||
boolean async = Boolean.parseBoolean(request.getParameter("async"));
|
||||
@@ -81,6 +81,12 @@ public class AutoJobAspect {
|
||||
"AutoJobAspect caught exception during job execution: {}",
|
||||
ex.getMessage(),
|
||||
ex);
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (ex instanceof RuntimeException) {
|
||||
throw (RuntimeException) ex;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
},
|
||||
@@ -109,7 +115,8 @@ public class AutoJobAspect {
|
||||
int maxRetries,
|
||||
boolean trackProgress,
|
||||
boolean queueable,
|
||||
int resourceWeight) {
|
||||
int resourceWeight)
|
||||
throws Exception {
|
||||
|
||||
// Keep jobId reference for progress tracking in TaskManager
|
||||
AtomicReference<String> jobIdRef = new AtomicReference<>();
|
||||
@@ -207,6 +214,12 @@ public class AutoJobAspect {
|
||||
|
||||
// If we get here, all retries failed
|
||||
if (lastException != null) {
|
||||
// Rethrow RuntimeException as-is to preserve exception type
|
||||
if (lastException instanceof RuntimeException) {
|
||||
throw (RuntimeException) lastException;
|
||||
}
|
||||
// Wrap checked exceptions - GlobalExceptionHandler will unwrap
|
||||
// BaseAppException
|
||||
throw new RuntimeException(
|
||||
"Job failed after "
|
||||
+ maxRetries
|
||||
|
||||
@@ -37,10 +37,6 @@ public class AppConfig {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@Getter
|
||||
@Value("${baseUrl:http://localhost}")
|
||||
private String baseUrl;
|
||||
|
||||
@Getter
|
||||
@Value("${server.servlet.context-path:/}")
|
||||
private String contextPath;
|
||||
@@ -49,6 +45,17 @@ public class AppConfig {
|
||||
@Value("${server.port:8080}")
|
||||
private String serverPort;
|
||||
|
||||
/**
|
||||
* Get the backend URL from system configuration. Falls back to http://localhost if not
|
||||
* configured.
|
||||
*
|
||||
* @return The backend base URL for SAML/OAuth/API callbacks
|
||||
*/
|
||||
public String getBackendUrl() {
|
||||
String backendUrl = applicationProperties.getSystem().getBackendUrl();
|
||||
return (backendUrl != null && !backendUrl.isBlank()) ? backendUrl : "http://localhost";
|
||||
}
|
||||
|
||||
@Value("${v2}")
|
||||
public boolean v2Enabled;
|
||||
|
||||
@@ -69,7 +76,7 @@ public class AppConfig {
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
return applicationProperties.getSecurity().getEnableLogin();
|
||||
return applicationProperties.getSecurity().isEnableLogin();
|
||||
}
|
||||
|
||||
@Bean(name = "appName")
|
||||
@@ -113,9 +120,7 @@ public class AppConfig {
|
||||
|
||||
@Bean(name = "enableAlphaFunctionality")
|
||||
public boolean enableAlphaFunctionality() {
|
||||
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
|
||||
? applicationProperties.getSystem().getEnableAlphaFunctionality()
|
||||
: false;
|
||||
return applicationProperties.getSystem().isEnableAlphaFunctionality();
|
||||
}
|
||||
|
||||
@Bean(name = "rateLimit")
|
||||
@@ -258,9 +263,14 @@ public class AppConfig {
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
@Bean(name = "disablePixel")
|
||||
public boolean disablePixel() {
|
||||
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
|
||||
@Bean(name = "scarfEnabled")
|
||||
public boolean scarfEnabled() {
|
||||
return applicationProperties.getSystem().isScarfEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "posthogEnabled")
|
||||
public boolean posthogEnabled() {
|
||||
return applicationProperties.getSystem().isPosthogEnabled();
|
||||
}
|
||||
|
||||
@Bean(name = "machineType")
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ package stirling.software.common.configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Locale;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -61,7 +62,7 @@ public class InstallationPathConfig {
|
||||
|
||||
private static String initializeBasePath() {
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
String os = System.getProperty("os.name").toLowerCase(Locale.ROOT);
|
||||
if (os.contains("win")) {
|
||||
return Paths.get(
|
||||
System.getenv("APPDATA"), // parent path
|
||||
|
||||
+40
-2
@@ -10,8 +10,10 @@ import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Operations;
|
||||
import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline;
|
||||
import stirling.software.common.model.ApplicationProperties.System;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@@ -19,8 +21,16 @@ import stirling.software.common.model.ApplicationProperties.CustomPaths.Pipeline
|
||||
public class RuntimePathConfig {
|
||||
private final ApplicationProperties properties;
|
||||
private final String basePath;
|
||||
|
||||
// Operation paths
|
||||
private final String weasyPrintPath;
|
||||
private final String unoConvertPath;
|
||||
private final String calibrePath;
|
||||
private final String ocrMyPdfPath;
|
||||
private final String sOfficePath;
|
||||
|
||||
// Tesseract data path
|
||||
private final String tessDataPath;
|
||||
|
||||
// Pipeline paths
|
||||
private final String pipelineWatchedFoldersPath;
|
||||
@@ -37,7 +47,10 @@ public class RuntimePathConfig {
|
||||
String defaultFinishedFolders = Path.of(this.pipelinePath, "finishedFolders").toString();
|
||||
String defaultWebUIConfigs = Path.of(this.pipelinePath, "defaultWebUIConfigs").toString();
|
||||
|
||||
Pipeline pipeline = properties.getSystem().getCustomPaths().getPipeline();
|
||||
System system = properties.getSystem();
|
||||
CustomPaths customPaths = system.getCustomPaths();
|
||||
|
||||
Pipeline pipeline = customPaths.getPipeline();
|
||||
|
||||
this.pipelineWatchedFoldersPath =
|
||||
resolvePath(
|
||||
@@ -57,8 +70,11 @@ public class RuntimePathConfig {
|
||||
// Initialize Operation paths
|
||||
String defaultWeasyPrintPath = isDocker ? "/opt/venv/bin/weasyprint" : "weasyprint";
|
||||
String defaultUnoConvertPath = isDocker ? "/opt/venv/bin/unoconvert" : "unoconvert";
|
||||
String defaultCalibrePath = isDocker ? "/opt/calibre/ebook-convert" : "ebook-convert";
|
||||
String defaultOcrMyPdfPath = isDocker ? "/usr/bin/ocrmypdf" : "ocrmypdf";
|
||||
String defaultSOfficePath = isDocker ? "/usr/bin/soffice" : "soffice";
|
||||
|
||||
Operations operations = properties.getSystem().getCustomPaths().getOperations();
|
||||
Operations operations = customPaths.getOperations();
|
||||
this.weasyPrintPath =
|
||||
resolvePath(
|
||||
defaultWeasyPrintPath,
|
||||
@@ -67,6 +83,28 @@ public class RuntimePathConfig {
|
||||
resolvePath(
|
||||
defaultUnoConvertPath,
|
||||
operations != null ? operations.getUnoconvert() : null);
|
||||
this.calibrePath =
|
||||
resolvePath(
|
||||
defaultCalibrePath, operations != null ? operations.getCalibre() : null);
|
||||
this.ocrMyPdfPath =
|
||||
resolvePath(
|
||||
defaultOcrMyPdfPath, operations != null ? operations.getOcrmypdf() : null);
|
||||
this.sOfficePath =
|
||||
resolvePath(
|
||||
defaultSOfficePath, operations != null ? operations.getSoffice() : null);
|
||||
|
||||
// Initialize Tesseract data path
|
||||
String defaultTessDataPath =
|
||||
isDocker ? "/usr/share/tesseract-ocr/5/tessdata" : "/usr/share/tessdata";
|
||||
|
||||
String tessPath = system.getTessdataDir();
|
||||
String tessdataDir = java.lang.System.getenv("TESSDATA_PREFIX");
|
||||
|
||||
this.tessDataPath =
|
||||
resolvePath(
|
||||
defaultTessDataPath,
|
||||
(tessPath != null && !tessPath.isEmpty()) ? tessPath : tessdataDir);
|
||||
log.info("Using Tesseract data path: {}", this.tessDataPath);
|
||||
}
|
||||
|
||||
private String resolvePath(String defaultPath, String customPath) {
|
||||
|
||||
+116
-18
@@ -12,6 +12,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -68,6 +69,7 @@ public class ApplicationProperties {
|
||||
|
||||
private AutoPipeline autoPipeline = new AutoPipeline();
|
||||
private ProcessExecutor processExecutor = new ProcessExecutor();
|
||||
private PdfEditor pdfEditor = new PdfEditor();
|
||||
|
||||
@Bean
|
||||
public PropertySource<?> dynamicYamlPropertySource(ConfigurableEnvironment environment)
|
||||
@@ -100,6 +102,46 @@ public class ApplicationProperties {
|
||||
private String outputFolder;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PdfEditor {
|
||||
private Cache cache = new Cache();
|
||||
private FontNormalization fontNormalization = new FontNormalization();
|
||||
private CffConverter cffConverter = new CffConverter();
|
||||
private Type3 type3 = new Type3();
|
||||
private String fallbackFont = "classpath:/static/fonts/NotoSans-Regular.ttf";
|
||||
|
||||
@Data
|
||||
public static class Cache {
|
||||
private long maxBytes = -1;
|
||||
private int maxPercent = 20;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FontNormalization {
|
||||
private boolean enabled = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class CffConverter {
|
||||
private boolean enabled = true;
|
||||
private String method = "python";
|
||||
private String pythonCommand = "/opt/venv/bin/python3";
|
||||
private String pythonScript = "/scripts/convert_cff_to_ttf.py";
|
||||
private String fontforgeCommand = "fontforge";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Type3 {
|
||||
private Library library = new Library();
|
||||
|
||||
@Data
|
||||
public static class Library {
|
||||
private boolean enabled = true;
|
||||
private String index = "classpath:/type3/library/index.json";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Legal {
|
||||
private String termsAndConditions;
|
||||
@@ -111,8 +153,7 @@ public class ApplicationProperties {
|
||||
|
||||
@Data
|
||||
public static class Security {
|
||||
private Boolean enableLogin;
|
||||
private Boolean csrfDisabled;
|
||||
private boolean enableLogin;
|
||||
private InitialLogin initialLogin = new InitialLogin();
|
||||
private OAUTH2 oauth2 = new OAUTH2();
|
||||
private SAML2 saml2 = new SAML2();
|
||||
@@ -287,7 +328,7 @@ public class ApplicationProperties {
|
||||
private KeycloakProvider keycloak = new KeycloakProvider();
|
||||
|
||||
public Provider get(String registrationId) throws UnsupportedProviderException {
|
||||
return switch (registrationId.toLowerCase()) {
|
||||
return switch (registrationId.toLowerCase(Locale.ROOT)) {
|
||||
case "google" -> getGoogle();
|
||||
case "github" -> getGithub();
|
||||
case "keycloak" -> getKeycloak();
|
||||
@@ -295,8 +336,8 @@ public class ApplicationProperties {
|
||||
throw new UnsupportedProviderException(
|
||||
"Logout from the provider "
|
||||
+ registrationId
|
||||
+ " is not supported. "
|
||||
+ "Report it at https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||
+ " is not supported. Report it at"
|
||||
+ " https://github.com/Stirling-Tools/Stirling-PDF/issues");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -349,29 +390,32 @@ public class ApplicationProperties {
|
||||
@Data
|
||||
public static class System {
|
||||
private String defaultLocale;
|
||||
private Boolean googlevisibility;
|
||||
private boolean googlevisibility;
|
||||
private boolean showUpdate;
|
||||
private Boolean showUpdateOnlyAdmin;
|
||||
private boolean showUpdateOnlyAdmin;
|
||||
private boolean customHTMLFiles;
|
||||
private String tessdataDir;
|
||||
private Boolean enableAlphaFunctionality;
|
||||
private boolean enableAlphaFunctionality;
|
||||
private Boolean enableAnalytics;
|
||||
private Boolean enablePosthog;
|
||||
private Boolean enableScarf;
|
||||
private Boolean enableDesktopInstallSlide;
|
||||
private Datasource datasource;
|
||||
private Boolean disableSanitize;
|
||||
private boolean disableSanitize;
|
||||
private int maxDPI;
|
||||
private Boolean enableUrlToPDF;
|
||||
private boolean enableUrlToPDF;
|
||||
private Html html = new Html();
|
||||
private CustomPaths customPaths = new CustomPaths();
|
||||
private String fileUploadLimit;
|
||||
private TempFileManagement tempFileManagement = new TempFileManagement();
|
||||
private DatabaseBackup databaseBackup = new DatabaseBackup();
|
||||
private List<String> corsAllowedOrigins = new ArrayList<>();
|
||||
private String
|
||||
frontendUrl; // Base URL for frontend (used for invite links, etc.). If not set,
|
||||
private String backendUrl; // Backend base URL for SAML/OAuth/API callbacks (e.g.
|
||||
// 'http://localhost:8080', 'https://api.example.com'). Required for
|
||||
// SSO.
|
||||
private String frontendUrl; // Frontend URL for invite email links (e.g.
|
||||
|
||||
// falls back to backend URL.
|
||||
// 'https://app.example.com'). If not set, falls back to backendUrl.
|
||||
|
||||
public boolean isAnalyticsEnabled() {
|
||||
return this.getEnableAnalytics() != null && this.getEnableAnalytics();
|
||||
@@ -411,6 +455,9 @@ public class ApplicationProperties {
|
||||
public static class Operations {
|
||||
private String weasyprint;
|
||||
private String unoconvert;
|
||||
private String calibre;
|
||||
private String ocrmypdf;
|
||||
private String soffice;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,10 +540,10 @@ public class ApplicationProperties {
|
||||
@Override
|
||||
public String toString() {
|
||||
return """
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
Driver {
|
||||
driverName='%s'
|
||||
}
|
||||
"""
|
||||
.formatted(driverName);
|
||||
}
|
||||
}
|
||||
@@ -528,7 +575,7 @@ public class ApplicationProperties {
|
||||
|
||||
@Data
|
||||
public static class Metrics {
|
||||
private Boolean enabled;
|
||||
private boolean enabled;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -536,6 +583,7 @@ public class ApplicationProperties {
|
||||
@ToString.Exclude private String key;
|
||||
private String UUID;
|
||||
private String appVersion;
|
||||
private Boolean isNewServer;
|
||||
}
|
||||
|
||||
// TODO: Remove post migration
|
||||
@@ -575,6 +623,16 @@ public class ApplicationProperties {
|
||||
private String username;
|
||||
@ToString.Exclude private String password;
|
||||
private String from;
|
||||
// STARTTLS upgrades a plain SMTP connection to TLS after connecting (RFC 3207)
|
||||
private Boolean startTlsEnable = true;
|
||||
private Boolean startTlsRequired;
|
||||
// SSL/TLS wrapper for implicit TLS (typically port 465)
|
||||
private Boolean sslEnable;
|
||||
// Hostnames or patterns (e.g., "smtp.example.com" or "*") to trust for TLS certificates;
|
||||
// defaults to "*" (trust all) when not set
|
||||
private String sslTrust;
|
||||
// Enables hostname verification for TLS connections
|
||||
private Boolean sslCheckServerIdentity;
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -614,6 +672,25 @@ public class ApplicationProperties {
|
||||
public static class EnterpriseFeatures {
|
||||
private PersistentMetrics persistentMetrics = new PersistentMetrics();
|
||||
private Audit audit = new Audit();
|
||||
private DatabaseNotifications databaseNotifications = new DatabaseNotifications();
|
||||
|
||||
@Data
|
||||
public static class DatabaseNotifications {
|
||||
private Backup backups = new Backup();
|
||||
private Imports imports = new Imports();
|
||||
|
||||
@Data
|
||||
public static class Backup {
|
||||
private boolean successful = false;
|
||||
private boolean failed = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Imports {
|
||||
private boolean successful = false;
|
||||
private boolean failed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Audit {
|
||||
@@ -643,10 +720,12 @@ public class ApplicationProperties {
|
||||
private int weasyPrintSessionLimit;
|
||||
private int installAppSessionLimit;
|
||||
private int calibreSessionLimit;
|
||||
private int imageMagickSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
private int ghostscriptSessionLimit;
|
||||
private int ocrMyPdfSessionLimit;
|
||||
private int ffmpegSessionLimit;
|
||||
|
||||
public int getQpdfSessionLimit() {
|
||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||
@@ -680,6 +759,10 @@ public class ApplicationProperties {
|
||||
return calibreSessionLimit > 0 ? calibreSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getImageMagickSessionLimit() {
|
||||
return imageMagickSessionLimit > 0 ? imageMagickSessionLimit : 4;
|
||||
}
|
||||
|
||||
public int getGhostscriptSessionLimit() {
|
||||
return ghostscriptSessionLimit > 0 ? ghostscriptSessionLimit : 8;
|
||||
}
|
||||
@@ -687,6 +770,10 @@ public class ApplicationProperties {
|
||||
public int getOcrMyPdfSessionLimit() {
|
||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||
}
|
||||
|
||||
public int getFfmpegSessionLimit() {
|
||||
return ffmpegSessionLimit > 0 ? ffmpegSessionLimit : 2;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@@ -709,10 +796,13 @@ public class ApplicationProperties {
|
||||
@JsonProperty("calibretimeoutMinutes")
|
||||
private long calibreTimeoutMinutes;
|
||||
|
||||
private long imageMagickTimeoutMinutes;
|
||||
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
private long ghostscriptTimeoutMinutes;
|
||||
private long ocrMyPdfTimeoutMinutes;
|
||||
private long ffmpegTimeoutMinutes;
|
||||
|
||||
public long getTesseractTimeoutMinutes() {
|
||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||
@@ -746,6 +836,10 @@ public class ApplicationProperties {
|
||||
return calibreTimeoutMinutes > 0 ? calibreTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getImageMagickTimeoutMinutes() {
|
||||
return imageMagickTimeoutMinutes > 0 ? imageMagickTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getGhostscriptTimeoutMinutes() {
|
||||
return ghostscriptTimeoutMinutes > 0 ? ghostscriptTimeoutMinutes : 30;
|
||||
}
|
||||
@@ -753,6 +847,10 @@ public class ApplicationProperties {
|
||||
public long getOcrMyPdfTimeoutMinutes() {
|
||||
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getFfmpegTimeoutMinutes() {
|
||||
return ffmpegTimeoutMinutes > 0 ? ffmpegTimeoutMinutes : 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,11 @@ public class FileInfo {
|
||||
// Formats the file size into a human-readable string.
|
||||
public String getFormattedFileSize() {
|
||||
if (fileSize >= 1024 * 1024 * 1024) {
|
||||
return String.format(Locale.US, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
|
||||
return String.format(Locale.ROOT, "%.2f GB", fileSize / (1024.0 * 1024 * 1024));
|
||||
} else if (fileSize >= 1024 * 1024) {
|
||||
return String.format(Locale.US, "%.2f MB", fileSize / (1024.0 * 1024));
|
||||
return String.format(Locale.ROOT, "%.2f MB", fileSize / (1024.0 * 1024));
|
||||
} else if (fileSize >= 1024) {
|
||||
return String.format(Locale.US, "%.2f KB", fileSize / 1024.0);
|
||||
return String.format(Locale.ROOT, "%.2f KB", fileSize / 1024.0);
|
||||
} else {
|
||||
return String.format("%d Bytes", fileSize);
|
||||
}
|
||||
|
||||
+3
-7
@@ -7,6 +7,7 @@ import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
@@ -249,7 +250,7 @@ public class CustomPDFDocumentFactory {
|
||||
log.debug(
|
||||
"Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB",
|
||||
actualFreeMemory / (1024 * 1024),
|
||||
String.format("%.2f", freeMemoryPercent),
|
||||
String.format(Locale.ROOT, "%.2f", freeMemoryPercent),
|
||||
usedMemory / (1024 * 1024),
|
||||
maxMemory / (1024 * 1024));
|
||||
|
||||
@@ -258,7 +259,7 @@ public class CustomPDFDocumentFactory {
|
||||
|| actualFreeMemory < MIN_FREE_MEMORY_BYTES) {
|
||||
log.debug(
|
||||
"Low memory detected ({}%), forcing file-based cache",
|
||||
String.format("%.2f", freeMemoryPercent));
|
||||
String.format(Locale.ROOT, "%.2f", freeMemoryPercent));
|
||||
return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly());
|
||||
} else if (contentSize < SMALL_FILE_THRESHOLD) {
|
||||
log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024);
|
||||
@@ -477,11 +478,6 @@ public class CustomPDFDocumentFactory {
|
||||
return file;
|
||||
}
|
||||
|
||||
/** Create a uniquely named temporary directory */
|
||||
private Path createTempDirectory(String prefix) throws IOException {
|
||||
return Files.createTempDirectory(prefix + tempCounter.incrementAndGet() + "-");
|
||||
}
|
||||
|
||||
/** Create new document bytes based on an existing document */
|
||||
public byte[] createNewBytesBasedOnOldDocument(byte[] oldDocument) throws IOException {
|
||||
try (PDDocument document = load(oldDocument)) {
|
||||
|
||||
@@ -253,6 +253,22 @@ public class JobExecutorService {
|
||||
log.error("Synchronous job timed out after {} ms", timeoutToUse);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "Job timed out after " + timeoutToUse + " ms"));
|
||||
} catch (RuntimeException e) {
|
||||
// Check if this is a wrapped typed exception that should be handled by
|
||||
// GlobalExceptionHandler
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof stirling.software.common.util.ExceptionUtils.BaseAppException
|
||||
|| cause
|
||||
instanceof
|
||||
stirling.software.common.util.ExceptionUtils
|
||||
.BaseValidationException) {
|
||||
// Rethrow so GlobalExceptionHandler can handle with proper HTTP status codes
|
||||
throw e;
|
||||
}
|
||||
// Handle other RuntimeExceptions as generic errors
|
||||
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("error", "Job failed: " + e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error executing synchronous job: {}", e.getMessage(), e);
|
||||
// Construct a JSON error response
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
|
||||
public interface LineArtConversionService {
|
||||
PDImageXObject convertImageToLineArt(
|
||||
PDDocument doc, PDImageXObject originalImage, double threshold, int edgeLevel)
|
||||
throws IOException;
|
||||
}
|
||||
@@ -253,11 +253,8 @@ public class PostHogService {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_enableLogin",
|
||||
applicationProperties.getSecurity().getEnableLogin());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_csrfDisabled",
|
||||
applicationProperties.getSecurity().getCsrfDisabled());
|
||||
applicationProperties.getSecurity().isEnableLogin());
|
||||
addIfNotEmpty(properties, "security_csrfDisabled", true);
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"security_loginAttemptCount",
|
||||
@@ -302,13 +299,13 @@ public class PostHogService {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_googlevisibility",
|
||||
applicationProperties.getSystem().getGooglevisibility());
|
||||
applicationProperties.getSystem().isGooglevisibility());
|
||||
addIfNotEmpty(
|
||||
properties, "system_showUpdate", applicationProperties.getSystem().isShowUpdate());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_showUpdateOnlyAdmin",
|
||||
applicationProperties.getSystem().getShowUpdateOnlyAdmin());
|
||||
applicationProperties.getSystem().isShowUpdateOnlyAdmin());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_customHTMLFiles",
|
||||
@@ -320,7 +317,7 @@ public class PostHogService {
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enableAlphaFunctionality",
|
||||
applicationProperties.getSystem().getEnableAlphaFunctionality());
|
||||
applicationProperties.getSystem().isEnableAlphaFunctionality());
|
||||
addIfNotEmpty(
|
||||
properties,
|
||||
"system_enableAnalytics",
|
||||
@@ -340,7 +337,7 @@ public class PostHogService {
|
||||
|
||||
// Capture Metrics properties
|
||||
addIfNotEmpty(
|
||||
properties, "metrics_enabled", applicationProperties.getMetrics().getEnabled());
|
||||
properties, "metrics_enabled", applicationProperties.getMetrics().isEnabled());
|
||||
|
||||
// Capture EnterpriseEdition properties
|
||||
addIfNotEmpty(
|
||||
|
||||
@@ -5,6 +5,7 @@ import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -173,8 +174,8 @@ public class ResourceMonitor {
|
||||
log.info("System resource status changed from {} to {}", oldStatus, newStatus);
|
||||
log.info(
|
||||
"Current metrics - CPU: {}%, Memory: {}%, Free Memory: {} MB",
|
||||
String.format("%.1f", cpuUsage * 100),
|
||||
String.format("%.1f", memoryUsage * 100),
|
||||
String.format(Locale.ROOT, "%.1f", cpuUsage * 100),
|
||||
String.format(Locale.ROOT, "%.1f", memoryUsage * 100),
|
||||
freeMemory / (1024 * 1024));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
||||
+8
-5
@@ -5,6 +5,7 @@ import java.net.Inet6Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.URI;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -83,7 +84,7 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.getAllowedDomains().contains(host.toLowerCase());
|
||||
return config.getAllowedDomains().contains(host.toLowerCase(Locale.ROOT));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to parse URL for MAX security check: {}", url, e);
|
||||
@@ -101,7 +102,7 @@ public class SsrfProtectionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
String hostLower = host.toLowerCase();
|
||||
String hostLower = host.toLowerCase(Locale.ROOT);
|
||||
|
||||
// Check explicit blocked domains
|
||||
if (config.getBlockedDomains().contains(hostLower)) {
|
||||
@@ -111,7 +112,7 @@ public class SsrfProtectionService {
|
||||
|
||||
// Check internal TLD patterns
|
||||
for (String tld : config.getInternalTlds()) {
|
||||
if (hostLower.endsWith(tld.toLowerCase())) {
|
||||
if (hostLower.endsWith(tld.toLowerCase(Locale.ROOT))) {
|
||||
log.debug("URL blocked by internal TLD pattern '{}': {}", tld, url);
|
||||
return false;
|
||||
}
|
||||
@@ -123,9 +124,11 @@ public class SsrfProtectionService {
|
||||
config.getAllowedDomains().stream()
|
||||
.anyMatch(
|
||||
domain ->
|
||||
hostLower.equals(domain.toLowerCase())
|
||||
hostLower.equals(domain.toLowerCase(Locale.ROOT))
|
||||
|| hostLower.endsWith(
|
||||
"." + domain.toLowerCase()));
|
||||
"."
|
||||
+ domain.toLowerCase(
|
||||
Locale.ROOT)));
|
||||
|
||||
if (!isAllowed) {
|
||||
log.debug("URL not in allowed domains list: {}", url);
|
||||
|
||||
@@ -7,6 +7,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -101,14 +102,16 @@ public class TaskManager {
|
||||
if (!extractedFiles.isEmpty()) {
|
||||
jobResult.completeWithFiles(extractedFiles);
|
||||
log.debug(
|
||||
"Set multiple file results for job ID: {} with {} files extracted from ZIP",
|
||||
"Set multiple file results for job ID: {} with {} files extracted from"
|
||||
+ " ZIP",
|
||||
jobId,
|
||||
extractedFiles.size());
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn(
|
||||
"Failed to extract ZIP file for job {}: {}. Falling back to single file result.",
|
||||
"Failed to extract ZIP file for job {}: {}. Falling back to single file"
|
||||
+ " result.",
|
||||
jobId,
|
||||
e.getMessage());
|
||||
}
|
||||
@@ -342,12 +345,12 @@ public class TaskManager {
|
||||
/** Check if a file is a ZIP file based on content type and filename */
|
||||
private boolean isZipFile(String contentType, String fileName) {
|
||||
if (contentType != null
|
||||
&& (contentType.equals("application/zip")
|
||||
|| contentType.equals("application/x-zip-compressed"))) {
|
||||
&& ("application/zip".equals(contentType)
|
||||
|| "application/x-zip-compressed".equals(contentType))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileName != null && fileName.toLowerCase().endsWith(".zip")) {
|
||||
if (fileName != null && fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -414,7 +417,7 @@ public class TaskManager {
|
||||
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
}
|
||||
|
||||
String lowerName = fileName.toLowerCase();
|
||||
String lowerName = fileName.toLowerCase(Locale.ROOT);
|
||||
if (lowerName.endsWith(".pdf")) {
|
||||
return MediaType.APPLICATION_PDF_VALUE;
|
||||
} else if (lowerName.endsWith(".txt")) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -58,31 +59,24 @@ public class CbrUtils {
|
||||
log.warn(
|
||||
"Failed to open CBR/RAR archive due to corrupt header: {}",
|
||||
e.getMessage());
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat",
|
||||
"Invalid or corrupted CBR/RAR archive. "
|
||||
+ "The file may be corrupted, use an unsupported RAR format (RAR5+), "
|
||||
+ "or may not be a valid RAR archive. "
|
||||
+ "Please ensure the file is a valid RAR archive.");
|
||||
throw ExceptionUtils.createCbrInvalidFormatException(null);
|
||||
} catch (RarException e) {
|
||||
log.warn("Failed to open CBR/RAR archive: {}", e.getMessage());
|
||||
String errorMessage;
|
||||
String exMessage = e.getMessage() != null ? e.getMessage() : "";
|
||||
|
||||
if (exMessage.contains("encrypted")) {
|
||||
errorMessage = "Encrypted CBR/RAR archives are not supported.";
|
||||
throw ExceptionUtils.createCbrEncryptedException();
|
||||
} else if (exMessage.isEmpty()) {
|
||||
errorMessage =
|
||||
"Invalid CBR/RAR archive. "
|
||||
+ "The file may be encrypted, corrupted, or use an unsupported format.";
|
||||
throw ExceptionUtils.createCbrInvalidFormatException(
|
||||
"Invalid CBR/RAR archive. The file may be encrypted, corrupted, or"
|
||||
+ " use an unsupported format.");
|
||||
} else {
|
||||
errorMessage =
|
||||
throw ExceptionUtils.createCbrInvalidFormatException(
|
||||
"Invalid CBR/RAR archive: "
|
||||
+ exMessage
|
||||
+ ". The file may be encrypted, corrupted, or use an unsupported format.";
|
||||
+ ". The file may be encrypted, corrupted, or use an"
|
||||
+ " unsupported format.");
|
||||
}
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidFormat", errorMessage);
|
||||
} catch (IOException e) {
|
||||
log.warn("IO error reading CBR/RAR archive: {}", e.getMessage());
|
||||
throw ExceptionUtils.createFileProcessingException("CBR extraction", e);
|
||||
@@ -121,7 +115,8 @@ public class CbrUtils {
|
||||
if (imageEntries.isEmpty()) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fileProcessing",
|
||||
"No valid images found in the CBR file. The archive may be empty or contain no supported image formats.");
|
||||
"No valid images found in the CBR file. The archive may be empty or"
|
||||
+ " contain no supported image formats.");
|
||||
}
|
||||
|
||||
for (ImageEntryData imageEntry : imageEntries) {
|
||||
@@ -146,7 +141,8 @@ public class CbrUtils {
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.fileProcessing",
|
||||
"No images could be processed from the CBR file. All images may be corrupted or in unsupported formats.");
|
||||
"No images could be processed from the CBR file. All images may be"
|
||||
+ " corrupted or in unsupported formats.");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@@ -159,7 +155,6 @@ public class CbrUtils {
|
||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||
} catch (IOException e) {
|
||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,17 +165,17 @@ public class CbrUtils {
|
||||
|
||||
private void validateCbrFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
throw ExceptionUtils.createFileNoNameException();
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
if (!"cbr".equals(extension) && !"rar".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a CBR or RAR archive");
|
||||
throw ExceptionUtils.createNotCbrFileException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +185,7 @@ public class CbrUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
return "cbr".equals(extension) || "rar".equals(extension);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@@ -55,10 +56,10 @@ public class CbzUtils {
|
||||
new java.io.FileInputStream(tempFile.getFile()));
|
||||
ZipInputStream zis = new ZipInputStream(bis)) {
|
||||
if (zis.getNextEntry() == null) {
|
||||
throw new IllegalArgumentException("Archive is empty or invalid ZIP");
|
||||
throw ExceptionUtils.createCbzEmptyException();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Invalid CBZ/ZIP archive", e);
|
||||
throw ExceptionUtils.createCbzInvalidFormatException(e);
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.createNewDocument();
|
||||
@@ -83,7 +84,7 @@ public class CbzUtils {
|
||||
Comparator.comparing(ImageEntryData::name, new NaturalOrderComparator()));
|
||||
|
||||
if (imageEntries.isEmpty()) {
|
||||
throw new IllegalArgumentException("No valid images found in the CBZ file");
|
||||
throw ExceptionUtils.createCbzNoImagesException();
|
||||
}
|
||||
|
||||
for (ImageEntryData imageEntry : imageEntries) {
|
||||
@@ -106,8 +107,7 @@ public class CbzUtils {
|
||||
}
|
||||
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No images could be processed from the CBZ file");
|
||||
throw ExceptionUtils.createCbzCorruptedImagesException();
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
document.save(baos);
|
||||
@@ -119,7 +119,6 @@ public class CbzUtils {
|
||||
return GeneralUtils.optimizePdfWithGhostscript(pdfBytes);
|
||||
} catch (IOException e) {
|
||||
log.warn("Ghostscript optimization failed, returning unoptimized PDF", e);
|
||||
return pdfBytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,17 +129,17 @@ public class CbzUtils {
|
||||
|
||||
private void validateCbzFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
throw ExceptionUtils.createFileNoNameException();
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
if (!"cbz".equals(extension) && !"zip".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a CBZ or ZIP archive");
|
||||
throw ExceptionUtils.createNotCbzFileException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +149,7 @@ public class CbzUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
return "cbz".equals(extension) || "zip".equals(extension);
|
||||
}
|
||||
|
||||
@@ -160,7 +159,7 @@ public class CbzUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
return "cbz".equals(extension)
|
||||
|| "zip".equals(extension)
|
||||
|| "cbr".equals(extension)
|
||||
|
||||
@@ -11,6 +11,8 @@ public class CheckProgramInstall {
|
||||
private static final List<String> PYTHON_COMMANDS = Arrays.asList("python3", "python");
|
||||
private static boolean pythonAvailableChecked = false;
|
||||
private static String availablePythonCommand = null;
|
||||
private static boolean ffmpegAvailableChecked = false;
|
||||
private static boolean ffmpegAvailable = false;
|
||||
|
||||
/**
|
||||
* Checks which Python command is available and returns it.
|
||||
@@ -56,4 +58,25 @@ public class CheckProgramInstall {
|
||||
public static boolean isPythonAvailable() {
|
||||
return getAvailablePythonCommand() != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if FFmpeg is available on the system.
|
||||
*
|
||||
* @return true if FFmpeg is installed and accessible, false otherwise.
|
||||
*/
|
||||
public static boolean isFfmpegAvailable() {
|
||||
if (!ffmpegAvailableChecked) {
|
||||
try {
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG)
|
||||
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||
ffmpegAvailable = true;
|
||||
} catch (IOException | InterruptedException e) {
|
||||
ffmpegAvailable = false;
|
||||
} finally {
|
||||
ffmpegAvailableChecked = true;
|
||||
}
|
||||
}
|
||||
return ffmpegAvailable;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,14 +58,11 @@ public class ChecksumUtils {
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static String checksum(InputStream is, String algorithm) throws IOException {
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32":
|
||||
return checksumChecksum(is, new CRC32());
|
||||
case "ADLER32":
|
||||
return checksumChecksum(is, new Adler32());
|
||||
default:
|
||||
return toHex(checksumBytes(is, algorithm));
|
||||
}
|
||||
return switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32" -> checksumChecksum(is, new CRC32());
|
||||
case "ADLER32" -> checksumChecksum(is, new Adler32());
|
||||
default -> toHex(checksumBytes(is, algorithm));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -98,14 +95,13 @@ public class ChecksumUtils {
|
||||
* @throws IOException if reading from the stream fails
|
||||
*/
|
||||
public static String checksumBase64(InputStream is, String algorithm) throws IOException {
|
||||
switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32":
|
||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
||||
case "ADLER32":
|
||||
return Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
||||
default:
|
||||
return Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
||||
}
|
||||
return switch (algorithm.toUpperCase(Locale.ROOT)) {
|
||||
case "CRC32" ->
|
||||
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new CRC32()));
|
||||
case "ADLER32" ->
|
||||
Base64.getEncoder().encodeToString(checksumChecksumBytes(is, new Adler32()));
|
||||
default -> Base64.getEncoder().encodeToString(checksumBytes(is, algorithm));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +175,7 @@ public class ChecksumUtils {
|
||||
for (Map.Entry<String, Checksum> entry : checksums.entrySet()) {
|
||||
// Keep value as long and mask to ensure unsigned hex formatting.
|
||||
long unsigned32 = entry.getValue().getValue() & UNSIGNED_32_BIT_MASK;
|
||||
results.put(entry.getKey(), String.format("%08x", unsigned32));
|
||||
results.put(entry.getKey(), String.format(Locale.ROOT, "%08x", unsigned32));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@@ -258,7 +254,7 @@ public class ChecksumUtils {
|
||||
}
|
||||
// Keep as long and mask to ensure correct unsigned representation.
|
||||
long unsigned32 = checksum.getValue() & UNSIGNED_32_BIT_MASK;
|
||||
return String.format("%08x", unsigned32);
|
||||
return String.format(Locale.ROOT, "%08x", unsigned32);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,7 +290,7 @@ public class ChecksumUtils {
|
||||
private static String toHex(byte[] hash) {
|
||||
StringBuilder sb = new StringBuilder(hash.length * 2);
|
||||
for (byte b : hash) {
|
||||
sb.append(String.format("%02x", b));
|
||||
sb.append(String.format(Locale.ROOT, "%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -62,8 +62,7 @@ public class CustomHtmlSanitizer {
|
||||
.and(new HtmlPolicyBuilder().disallowElements("noscript").toFactory());
|
||||
|
||||
public String sanitize(String html) {
|
||||
boolean disableSanitize =
|
||||
Boolean.TRUE.equals(applicationProperties.getSystem().getDisableSanitize());
|
||||
boolean disableSanitize = applicationProperties.getSystem().isDisableSanitize();
|
||||
return disableSanitize ? html : POLICY.sanitize(html);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -229,7 +230,8 @@ public class EmlParser {
|
||||
Method getContentType = message.getClass().getMethod("getContentType");
|
||||
String contentType = (String) getContentType.invoke(message);
|
||||
|
||||
if (contentType != null && contentType.toLowerCase().contains(TEXT_HTML)) {
|
||||
if (contentType != null
|
||||
&& contentType.toLowerCase(Locale.ROOT).contains(TEXT_HTML)) {
|
||||
content.setHtmlBody(stringContent);
|
||||
} else {
|
||||
content.setTextBody(stringContent);
|
||||
@@ -296,7 +298,7 @@ public class EmlParser {
|
||||
String contentType = (String) getContentType.invoke(part);
|
||||
|
||||
String normalizedDisposition =
|
||||
disposition != null ? ((String) disposition).toLowerCase() : null;
|
||||
disposition != null ? ((String) disposition).toLowerCase(Locale.ROOT) : null;
|
||||
|
||||
if ((Boolean) isMimeType.invoke(part, TEXT_PLAIN) && normalizedDisposition == null) {
|
||||
Object partContent = getContent.invoke(part);
|
||||
@@ -422,7 +424,7 @@ public class EmlParser {
|
||||
RegexPatternUtils.getInstance().getNewlineSplitPattern().split(emlContent);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
if (line.toLowerCase().startsWith(headerName.toLowerCase())) {
|
||||
if (line.toLowerCase(Locale.ROOT).startsWith(headerName.toLowerCase(Locale.ROOT))) {
|
||||
StringBuilder value =
|
||||
new StringBuilder(line.substring(headerName.length()).trim());
|
||||
for (int j = i + 1; j < lines.length; j++) {
|
||||
@@ -444,7 +446,7 @@ public class EmlParser {
|
||||
|
||||
private static String extractHtmlBody(String emlContent) {
|
||||
try {
|
||||
String lowerContent = emlContent.toLowerCase();
|
||||
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
|
||||
int htmlStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_HTML);
|
||||
if (htmlStart == -1) return null;
|
||||
|
||||
@@ -463,7 +465,7 @@ public class EmlParser {
|
||||
|
||||
private static String extractTextBody(String emlContent) {
|
||||
try {
|
||||
String lowerContent = emlContent.toLowerCase();
|
||||
String lowerContent = emlContent.toLowerCase(Locale.ROOT);
|
||||
int textStart = lowerContent.indexOf(HEADER_CONTENT_TYPE + " " + TEXT_PLAIN);
|
||||
if (textStart == -1) {
|
||||
int bodyStart = emlContent.indexOf("\r\n\r\n");
|
||||
@@ -516,7 +518,7 @@ public class EmlParser {
|
||||
String currentEncoding = "";
|
||||
|
||||
for (String line : lines) {
|
||||
String lowerLine = line.toLowerCase().trim();
|
||||
String lowerLine = line.toLowerCase(Locale.ROOT).trim();
|
||||
|
||||
if (line.trim().isEmpty()) {
|
||||
inHeaders = false;
|
||||
@@ -554,9 +556,12 @@ public class EmlParser {
|
||||
}
|
||||
|
||||
private static boolean isAttachment(String disposition, String filename, String contentType) {
|
||||
return (disposition.toLowerCase().contains(DISPOSITION_ATTACHMENT) && !filename.isEmpty())
|
||||
|| (!filename.isEmpty() && !contentType.toLowerCase().startsWith("text/"))
|
||||
|| (contentType.toLowerCase().contains("application/") && !filename.isEmpty());
|
||||
return (disposition.toLowerCase(Locale.ROOT).contains(DISPOSITION_ATTACHMENT)
|
||||
&& !filename.isEmpty())
|
||||
|| (!filename.isEmpty()
|
||||
&& !contentType.toLowerCase(Locale.ROOT).startsWith("text/"))
|
||||
|| (contentType.toLowerCase(Locale.ROOT).contains("application/")
|
||||
&& !filename.isEmpty());
|
||||
}
|
||||
|
||||
private static String extractFilenameFromDisposition(String disposition) {
|
||||
@@ -565,8 +570,8 @@ public class EmlParser {
|
||||
}
|
||||
|
||||
// Handle filename*= (RFC 2231 encoded filename)
|
||||
if (disposition.toLowerCase().contains("filename*=")) {
|
||||
int filenameStarStart = disposition.toLowerCase().indexOf("filename*=") + 10;
|
||||
if (disposition.toLowerCase(Locale.ROOT).contains("filename*=")) {
|
||||
int filenameStarStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename*=") + 10;
|
||||
int filenameStarEnd = disposition.indexOf(";", filenameStarStart);
|
||||
if (filenameStarEnd == -1) filenameStarEnd = disposition.length();
|
||||
String extendedFilename =
|
||||
@@ -586,7 +591,7 @@ public class EmlParser {
|
||||
}
|
||||
|
||||
// Handle regular filename=
|
||||
int filenameStart = disposition.toLowerCase().indexOf("filename=") + 9;
|
||||
int filenameStart = disposition.toLowerCase(Locale.ROOT).indexOf("filename=") + 9;
|
||||
int filenameEnd = disposition.indexOf(";", filenameStart);
|
||||
if (filenameEnd == -1) filenameEnd = disposition.length();
|
||||
String filename = disposition.substring(filenameStart, filenameEnd).trim();
|
||||
|
||||
@@ -48,11 +48,11 @@ public class EmlProcessingUtils {
|
||||
|
||||
public static void validateEmlInput(byte[] emlBytes) {
|
||||
if (emlBytes == null || emlBytes.length == 0) {
|
||||
throw new IllegalArgumentException("EML file is empty or null");
|
||||
throw ExceptionUtils.createEmlEmptyException();
|
||||
}
|
||||
|
||||
if (isInvalidEmlFormat(emlBytes)) {
|
||||
throw new IllegalArgumentException("Invalid EML file format");
|
||||
throw ExceptionUtils.createEmlInvalidFormatException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,12 +109,13 @@ public class EmlProcessingUtils {
|
||||
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8">
|
||||
<title>%s</title>
|
||||
<style>
|
||||
""",
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8">
|
||||
<title>%s</title>
|
||||
<style>
|
||||
""",
|
||||
sanitizeText(content.getSubject(), customHtmlSanitizer)));
|
||||
|
||||
appendEnhancedStyles(html);
|
||||
@@ -127,14 +128,15 @@ public class EmlProcessingUtils {
|
||||
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>%s</h1>
|
||||
<div class="email-meta">
|
||||
<div><strong>From:</strong> %s</div>
|
||||
<div><strong>To:</strong> %s</div>
|
||||
""",
|
||||
<div class="email-container">
|
||||
<div class="email-header">
|
||||
<h1>%s</h1>
|
||||
<div class="email-meta">
|
||||
<div><strong>From:</strong> %s</div>
|
||||
<div><strong>To:</strong> %s</div>
|
||||
""",
|
||||
sanitizeText(content.getSubject(), customHtmlSanitizer),
|
||||
sanitizeText(content.getFrom(), customHtmlSanitizer),
|
||||
sanitizeText(content.getTo(), customHtmlSanitizer)));
|
||||
@@ -142,6 +144,7 @@ public class EmlProcessingUtils {
|
||||
if (content.getCc() != null && !content.getCc().trim().isEmpty()) {
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"<div><strong>CC:</strong> %s</div>\n",
|
||||
sanitizeText(content.getCc(), customHtmlSanitizer)));
|
||||
}
|
||||
@@ -149,6 +152,7 @@ public class EmlProcessingUtils {
|
||||
if (content.getBcc() != null && !content.getBcc().trim().isEmpty()) {
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"<div><strong>BCC:</strong> %s</div>\n",
|
||||
sanitizeText(content.getBcc(), customHtmlSanitizer)));
|
||||
}
|
||||
@@ -156,11 +160,13 @@ public class EmlProcessingUtils {
|
||||
if (content.getDate() != null) {
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"<div><strong>Date:</strong> %s</div>\n",
|
||||
PdfAttachmentHandler.formatEmailDate(content.getDate())));
|
||||
} else if (content.getDateString() != null && !content.getDateString().trim().isEmpty()) {
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"<div><strong>Date:</strong> %s</div>\n",
|
||||
sanitizeText(content.getDateString(), customHtmlSanitizer)));
|
||||
}
|
||||
@@ -175,6 +181,7 @@ public class EmlProcessingUtils {
|
||||
} else if (content.getTextBody() != null && !content.getTextBody().trim().isEmpty()) {
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"<div class=\"text-body\">%s</div>",
|
||||
convertTextToHtml(content.getTextBody(), customHtmlSanitizer)));
|
||||
} else {
|
||||
@@ -234,14 +241,16 @@ public class EmlProcessingUtils {
|
||||
.getUrlLinkPattern()
|
||||
.matcher(html)
|
||||
.replaceAll(
|
||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
"<a href=\"$1\" style=\"color: #1a73e8; text-decoration:"
|
||||
+ " underline;\">$1</a>");
|
||||
|
||||
html =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getEmailLinkPattern()
|
||||
.matcher(html)
|
||||
.replaceAll(
|
||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration: underline;\">$1</a>");
|
||||
"<a href=\"mailto:$1\" style=\"color: #1a73e8; text-decoration:"
|
||||
+ " underline;\">$1</a>");
|
||||
|
||||
return html;
|
||||
}
|
||||
@@ -249,127 +258,128 @@ public class EmlProcessingUtils {
|
||||
private static void appendEnhancedStyles(StringBuilder html) {
|
||||
String css =
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
body {
|
||||
font-family: %s;
|
||||
font-size: %dpx;
|
||||
line-height: %s;
|
||||
color: %s;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background-color: %s;
|
||||
}
|
||||
body {
|
||||
font-family: %s;
|
||||
font-size: %dpx;
|
||||
line-height: %s;
|
||||
color: %s;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background-color: %s;
|
||||
}
|
||||
|
||||
.email-container {
|
||||
width: 100%%;
|
||||
max-width: 100%%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.email-container {
|
||||
width: 100%%;
|
||||
max-width: 100%%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.email-header {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid %s;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.email-header {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid %s;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.email-header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: %dpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
.email-header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: %dpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.email-meta div {
|
||||
margin-bottom: 2px;
|
||||
font-size: %dpx;
|
||||
}
|
||||
.email-meta div {
|
||||
margin-bottom: 2px;
|
||||
font-size: %dpx;
|
||||
}
|
||||
|
||||
.email-body {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.email-body {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.attachment-section {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: %s;
|
||||
border: 1px solid %s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.attachment-section {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: %s;
|
||||
border: 1px solid %s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.attachment-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: %dpx;
|
||||
}
|
||||
.attachment-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: %dpx;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.attachment-item {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.attachment-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.attachment-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.attachment-details, .attachment-type {
|
||||
font-size: %dpx;
|
||||
color: #555555;
|
||||
}
|
||||
.attachment-details, .attachment-type {
|
||||
font-size: %dpx;
|
||||
color: #555555;
|
||||
}
|
||||
|
||||
.attachment-inclusion-note, .attachment-info-note {
|
||||
margin-top: 8px;
|
||||
padding: 6px;
|
||||
font-size: %dpx;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.attachment-inclusion-note, .attachment-info-note {
|
||||
margin-top: 8px;
|
||||
padding: 6px;
|
||||
font-size: %dpx;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.attachment-inclusion-note {
|
||||
background-color: #e6ffed;
|
||||
border: 1px solid #d4f7dc;
|
||||
color: #006420;
|
||||
}
|
||||
.attachment-inclusion-note {
|
||||
background-color: #e6ffed;
|
||||
border: 1px solid #d4f7dc;
|
||||
color: #006420;
|
||||
}
|
||||
|
||||
.attachment-info-note {
|
||||
background-color: #fff9e6;
|
||||
border: 1px solid #fff0c2;
|
||||
color: #664d00;
|
||||
}
|
||||
.attachment-info-note {
|
||||
background-color: #fff9e6;
|
||||
border: 1px solid #fff0c2;
|
||||
color: #664d00;
|
||||
}
|
||||
|
||||
.attachment-link-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
.attachment-link-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.attachment-link-container:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
.attachment-link-container:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.attachment-note {
|
||||
font-size: %dpx;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.attachment-note {
|
||||
font-size: %dpx;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.no-content {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
.no-content {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.text-body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.text-body {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
""",
|
||||
img {
|
||||
max-width: 100%%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
""",
|
||||
DEFAULT_FONT_FAMILY,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
@@ -420,13 +430,14 @@ public class EmlProcessingUtils {
|
||||
String attachmentId = "attachment_" + i;
|
||||
html.append(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
<div class="attachment-item" id="%s">
|
||||
<span class="attachment-icon" data-filename="%s">@</span>
|
||||
<span class="attachment-name">%s</span>
|
||||
<span class="attachment-details">(%s%s)</span>
|
||||
</div>
|
||||
""",
|
||||
<div class="attachment-item" id="%s">
|
||||
<span class="attachment-icon" data-filename="%s">@</span>
|
||||
<span class="attachment-name">%s</span>
|
||||
<span class="attachment-details">(%s%s)</span>
|
||||
</div>
|
||||
""",
|
||||
attachmentId,
|
||||
escapeHtml(embeddedFilename),
|
||||
escapeHtml(EmlParser.safeMimeDecode(attachment.getFilename())),
|
||||
@@ -470,7 +481,7 @@ public class EmlProcessingUtils {
|
||||
}
|
||||
|
||||
if (filename != null) {
|
||||
String lowerFilename = filename.toLowerCase();
|
||||
String lowerFilename = filename.toLowerCase(Locale.ROOT);
|
||||
for (Map.Entry<String, String> entry : EXTENSION_TO_MIME_TYPE.entrySet()) {
|
||||
if (lowerFilename.endsWith(entry.getKey())) {
|
||||
return entry.getValue();
|
||||
@@ -516,7 +527,7 @@ public class EmlProcessingUtils {
|
||||
result.append(processedText, lastEnd, matcher.start());
|
||||
|
||||
String charset = matcher.group(1);
|
||||
String encoding = matcher.group(2).toUpperCase();
|
||||
String encoding = matcher.group(2).toUpperCase(Locale.ROOT);
|
||||
String encodedValue = matcher.group(3);
|
||||
|
||||
try {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,11 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@@ -37,9 +35,11 @@ public class FileToPdf {
|
||||
try (TempFile tempInputFile =
|
||||
new TempFile(
|
||||
tempFileManager,
|
||||
fileName.toLowerCase().endsWith(".html") ? ".html" : ".zip")) {
|
||||
fileName.toLowerCase(Locale.ROOT).endsWith(".html")
|
||||
? ".html"
|
||||
: ".zip")) {
|
||||
|
||||
if (fileName.toLowerCase().endsWith(".html")) {
|
||||
if (fileName.toLowerCase(Locale.ROOT).endsWith(".html")) {
|
||||
String sanitizedHtml =
|
||||
sanitizeHtmlContent(
|
||||
new String(fileBytes, StandardCharsets.UTF_8),
|
||||
@@ -47,7 +47,7 @@ public class FileToPdf {
|
||||
Files.write(
|
||||
tempInputFile.getPath(),
|
||||
sanitizedHtml.getBytes(StandardCharsets.UTF_8));
|
||||
} else if (fileName.toLowerCase().endsWith(".zip")) {
|
||||
} else if (fileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
|
||||
Files.write(tempInputFile.getPath(), fileBytes);
|
||||
sanitizeHtmlFilesInZip(
|
||||
tempInputFile.getPath(), tempFileManager, customHtmlSanitizer);
|
||||
@@ -102,8 +102,8 @@ public class FileToPdf {
|
||||
tempUnzippedDir.getPath().resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (!entry.isDirectory()) {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
if (entry.getName().toLowerCase().endsWith(".html")
|
||||
|| entry.getName().toLowerCase().endsWith(".htm")) {
|
||||
if (entry.getName().toLowerCase(Locale.ROOT).endsWith(".html")
|
||||
|| entry.getName().toLowerCase(Locale.ROOT).endsWith(".htm")) {
|
||||
String content =
|
||||
new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
|
||||
String sanitizedContent =
|
||||
@@ -145,64 +145,6 @@ public class FileToPdf {
|
||||
}
|
||||
}
|
||||
|
||||
private static void deleteDirectory(Path dir) throws IOException {
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc)
|
||||
throws IOException {
|
||||
Files.delete(dir);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException {
|
||||
Path tempDirectory = Files.createTempDirectory("unzipped_");
|
||||
try (ZipInputStream zipIn =
|
||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
||||
} else {
|
||||
Files.createDirectories(
|
||||
filePath.getParent()); // Create parent directories if they don't exist
|
||||
Files.copy(zipIn, filePath);
|
||||
}
|
||||
zipIn.closeEntry();
|
||||
entry = zipIn.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// Search for the main HTML file.
|
||||
try (Stream<Path> walk = Files.walk(tempDirectory)) {
|
||||
List<Path> htmlFiles = walk.filter(file -> file.toString().endsWith(".html")).toList();
|
||||
|
||||
if (htmlFiles.isEmpty()) {
|
||||
throw new IOException("No HTML files found in the unzipped directory.");
|
||||
}
|
||||
|
||||
// Prioritize 'index.html' if it exists, otherwise use the first .html file
|
||||
for (Path htmlFile : htmlFiles) {
|
||||
if ("index.html".equals(htmlFile.getFileName().toString())) {
|
||||
return htmlFile;
|
||||
}
|
||||
}
|
||||
|
||||
return htmlFiles.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
static String sanitizeZipFilename(String entryName) {
|
||||
if (entryName == null || entryName.trim().isEmpty()) {
|
||||
return "";
|
||||
|
||||
+9
-4
@@ -1,4 +1,4 @@
|
||||
package stirling.software.proprietary.util;
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
@@ -23,9 +23,13 @@ import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Utility class for copying and transforming PDF form fields during page operations. Used by
|
||||
* multi-page layout and other page manipulation operations that need to preserve form fields.
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class FormCopyUtils {
|
||||
public class GeneralFormCopyUtils {
|
||||
|
||||
public boolean hasAnyRotatedPage(PDDocument document) {
|
||||
try {
|
||||
@@ -188,7 +192,8 @@ public class FormCopyUtils {
|
||||
continue;
|
||||
}
|
||||
|
||||
FormFieldTypeSupport handler = FormFieldTypeSupport.forField(terminalField);
|
||||
GeneralFormFieldTypeSupport handler =
|
||||
GeneralFormFieldTypeSupport.forField(terminalField);
|
||||
if (handler == null) {
|
||||
log.debug(
|
||||
"Skipping unsupported field type '{}' for widget '{}'",
|
||||
@@ -222,7 +227,7 @@ public class FormCopyUtils {
|
||||
}
|
||||
|
||||
private void copyFieldUsingHandler(
|
||||
FormFieldTypeSupport handler,
|
||||
GeneralFormFieldTypeSupport handler,
|
||||
PDTerminalField sourceField,
|
||||
PDAcroForm newAcroForm,
|
||||
PDPage destinationPage,
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDCheckBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDComboBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDListBox;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDPushButton;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDRadioButton;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTerminalField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Simplified form field type support for general PDF operations. This is a subset of the full
|
||||
* proprietary FormFieldTypeSupport, containing only what's needed for basic form field copying
|
||||
* during page operations.
|
||||
*/
|
||||
@Slf4j
|
||||
public enum GeneralFormFieldTypeSupport {
|
||||
TEXT("text", "textField", PDTextField.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
PDTextField textField = new PDTextField(acroForm);
|
||||
textField.setDefaultAppearance("/Helv 12 Tf 0 g");
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDTextField src = (PDTextField) source;
|
||||
PDTextField dst = (PDTextField) target;
|
||||
String value = src.getValueAsString();
|
||||
if (value != null) {
|
||||
dst.setValue(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
CHECKBOX("checkbox", "checkBox", PDCheckBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDCheckBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDCheckBox src = (PDCheckBox) source;
|
||||
PDCheckBox dst = (PDCheckBox) target;
|
||||
if (src.isChecked()) {
|
||||
dst.check();
|
||||
} else {
|
||||
dst.unCheck();
|
||||
}
|
||||
}
|
||||
},
|
||||
RADIO("radio", "radioButton", PDRadioButton.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDRadioButton(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDRadioButton src = (PDRadioButton) source;
|
||||
PDRadioButton dst = (PDRadioButton) target;
|
||||
if (src.getExportValues() != null) {
|
||||
dst.setExportValues(src.getExportValues());
|
||||
}
|
||||
if (src.getValue() != null) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
},
|
||||
COMBOBOX("combobox", "comboBox", PDComboBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDComboBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDComboBox src = (PDComboBox) source;
|
||||
PDComboBox dst = (PDComboBox) target;
|
||||
if (src.getOptions() != null) {
|
||||
dst.setOptions(src.getOptions());
|
||||
}
|
||||
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
},
|
||||
LISTBOX("listbox", "listBox", PDListBox.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDListBox(acroForm);
|
||||
}
|
||||
|
||||
@Override
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
PDListBox src = (PDListBox) source;
|
||||
PDListBox dst = (PDListBox) target;
|
||||
if (src.getOptions() != null) {
|
||||
dst.setOptions(src.getOptions());
|
||||
}
|
||||
if (src.getValue() != null && !src.getValue().isEmpty()) {
|
||||
dst.setValue(src.getValue());
|
||||
}
|
||||
}
|
||||
},
|
||||
SIGNATURE("signature", "signature", PDSignatureField.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDSignatureField(acroForm);
|
||||
}
|
||||
},
|
||||
BUTTON("button", "pushButton", PDPushButton.class) {
|
||||
@Override
|
||||
PDTerminalField createField(PDAcroForm acroForm) {
|
||||
return new PDPushButton(acroForm);
|
||||
}
|
||||
};
|
||||
|
||||
private final String typeName;
|
||||
private final String fallbackWidgetName;
|
||||
private final Class<? extends PDTerminalField> fieldClass;
|
||||
|
||||
GeneralFormFieldTypeSupport(
|
||||
String typeName,
|
||||
String fallbackWidgetName,
|
||||
Class<? extends PDTerminalField> fieldClass) {
|
||||
this.typeName = typeName;
|
||||
this.fallbackWidgetName = fallbackWidgetName;
|
||||
this.fieldClass = fieldClass;
|
||||
}
|
||||
|
||||
public static GeneralFormFieldTypeSupport forField(PDField field) {
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
for (GeneralFormFieldTypeSupport handler : values()) {
|
||||
if (handler.fieldClass.isInstance(field)) {
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String typeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
String fallbackWidgetName() {
|
||||
return fallbackWidgetName;
|
||||
}
|
||||
|
||||
abstract PDTerminalField createField(PDAcroForm acroForm);
|
||||
|
||||
void copyFromOriginal(PDTerminalField source, PDTerminalField target) throws IOException {
|
||||
// default no-op
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ import stirling.software.common.configuration.InstallationPathConfig;
|
||||
@UtilityClass
|
||||
public class GeneralUtils {
|
||||
|
||||
/**
|
||||
* Maximum number of resolved DNS addresses allowed for a host before it is considered unsafe.
|
||||
*/
|
||||
private static final int MAX_DNS_ADDRESSES = 20;
|
||||
|
||||
private final Set<String> DEFAULT_VALID_SCRIPTS = Set.of("png_to_webp.py", "split_photos.py");
|
||||
private final Set<String> DEFAULT_VALID_PIPELINE =
|
||||
Set.of(
|
||||
@@ -90,32 +95,6 @@ public class GeneralUtils {
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/*
|
||||
* Gets the configured temporary directory, creating it if necessary.
|
||||
*
|
||||
* @return Path to the temporary directory
|
||||
* @throws IOException if directory creation fails
|
||||
*/
|
||||
private Path getTempDirectory() throws IOException {
|
||||
String customTempDir = System.getenv("STIRLING_TEMPFILES_DIRECTORY");
|
||||
if (customTempDir == null || customTempDir.isEmpty()) {
|
||||
customTempDir = System.getProperty("stirling.tempfiles.directory");
|
||||
}
|
||||
|
||||
Path tempDir;
|
||||
if (customTempDir != null && !customTempDir.isEmpty()) {
|
||||
tempDir = Path.of(customTempDir);
|
||||
} else {
|
||||
tempDir = Path.of(System.getProperty("java.io.tmpdir"), "stirling-pdf");
|
||||
}
|
||||
|
||||
if (!Files.exists(tempDir)) {
|
||||
Files.createDirectories(tempDir);
|
||||
}
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove file extension
|
||||
*
|
||||
@@ -154,7 +133,7 @@ public class GeneralUtils {
|
||||
return matcher.find() ? matcher.replaceFirst("") : filename;
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Append suffix to base name with null safety.
|
||||
*
|
||||
* @param baseName the base filename, null becomes "default"
|
||||
@@ -165,7 +144,7 @@ public class GeneralUtils {
|
||||
return (baseName == null ? "default" : baseName) + (suffix != null ? suffix : "");
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Generate a PDF filename by removing extension from first file and adding suffix.
|
||||
*
|
||||
* <p>High-level utility method for common PDF naming scenarios. Handles null safety and uses
|
||||
@@ -180,7 +159,7 @@ public class GeneralUtils {
|
||||
return appendSuffix(baseName, suffix);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Process a list of filenames by removing extensions and adding suffix.
|
||||
*
|
||||
* <p>Efficiently processes multiple filenames using streaming operations and bulk operations
|
||||
@@ -201,7 +180,7 @@ public class GeneralUtils {
|
||||
.forEach(processor);
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Extract title from filename by removing extension, with fallback handling.
|
||||
*
|
||||
* <p>Returns "Untitled" for null or empty filenames, otherwise removes the extension using the
|
||||
@@ -269,6 +248,12 @@ public class GeneralUtils {
|
||||
.getResources(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates URL syntax and disallows common-infrastructure targets to reduce SSRF risk.
|
||||
*
|
||||
* @param urlStr a URL string to validate
|
||||
* @return {@code true} if the URL is syntactically valid and allowed; {@code false} otherwise
|
||||
*/
|
||||
public boolean isValidURL(String urlStr) {
|
||||
try {
|
||||
Urls.create(
|
||||
@@ -279,7 +264,7 @@ public class GeneralUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Checks if a URL is reachable with proper timeout configuration and error handling.
|
||||
*
|
||||
* @param urlStr the URL string to check
|
||||
@@ -289,15 +274,17 @@ public class GeneralUtils {
|
||||
return isURLReachable(urlStr, 5000, 5000);
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if a URL is reachable with configurable timeouts.
|
||||
/**
|
||||
* Checks whether a URL is reachable using configurable timeouts. Only {@code http} and {@code
|
||||
* https} protocols are permitted, and local/private/multicast ranges are blocked.
|
||||
*
|
||||
* @param urlStr the URL string to check
|
||||
* @param urlStr the URL to probe
|
||||
* @param connectTimeout connection timeout in milliseconds
|
||||
* @param readTimeout read timeout in milliseconds
|
||||
* @return true if URL is reachable, false otherwise
|
||||
* @return {@code true} if a HEAD request returns a 2xx or 3xx status; {@code false} otherwise
|
||||
*/
|
||||
public boolean isURLReachable(String urlStr, int connectTimeout, int readTimeout) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// Parse the URL
|
||||
URL url = URI.create(urlStr).toURL();
|
||||
@@ -308,14 +295,17 @@ public class GeneralUtils {
|
||||
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
|
||||
if (host == null || host.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isDisallowedNetworkLocation(host)) {
|
||||
return false; // Exclude local, private or otherwise sensitive addresses
|
||||
}
|
||||
|
||||
// Check if the URL is reachable
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("HEAD");
|
||||
connection.setConnectTimeout(connectTimeout);
|
||||
connection.setReadTimeout(readTimeout);
|
||||
@@ -326,29 +316,173 @@ public class GeneralUtils {
|
||||
} catch (Exception e) {
|
||||
log.debug("URL {} is not reachable: {}", urlStr, e.getMessage());
|
||||
return false; // Return false in case of any exception
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocalAddress(String host) {
|
||||
/**
|
||||
* Determines whether the specified host resolves to a disallowed network location, such as
|
||||
* local, private, multicast, or reserved ranges. Excessive DNS results are also blocked.
|
||||
*
|
||||
* @param host the hostname to resolve
|
||||
* @return {@code true} if the host should be considered unsafe
|
||||
*/
|
||||
private boolean isDisallowedNetworkLocation(String host) {
|
||||
// Resolution is delegated to the JVM/OS resolver which already applies system
|
||||
// configured query limits and timeouts. We only need the resolved addresses here so
|
||||
// that we can enforce the MAX_DNS_ADDRESSES limit and perform the sensitive range
|
||||
// checks below.
|
||||
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
|
||||
InetAddress[] addresses = InetAddress.getAllByName(host);
|
||||
if (addresses.length > MAX_DNS_ADDRESSES) {
|
||||
log.debug(
|
||||
"Blocking URL to host {} due to excessive DNS records (>{})",
|
||||
host,
|
||||
MAX_DNS_ADDRESSES);
|
||||
return true;
|
||||
}
|
||||
for (InetAddress address : addresses) {
|
||||
if (address == null || isSensitiveAddress(address)) {
|
||||
log.debug("Blocking URL to host {} resolved to {}", host, address);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return false; // Return false for invalid or unresolved addresses
|
||||
log.debug("Unable to resolve host {}: {}", host, e.getMessage());
|
||||
return true; // Treat resolution issues as unsafe to avoid SSRF
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given IP address lies within ranges that should not be contacted by the
|
||||
* server (loopback, link-local, private, multicast, etc.). IPv6 ULA and IPv4-mapped addresses
|
||||
* are handled.
|
||||
*
|
||||
* @param address the resolved address
|
||||
* @return {@code true} if the address is considered sensitive
|
||||
*/
|
||||
private boolean isSensitiveAddress(InetAddress address) {
|
||||
if (address.isAnyLocalAddress()
|
||||
|| address.isLoopbackAddress()
|
||||
|| address.isLinkLocalAddress()
|
||||
|| address.isSiteLocalAddress()
|
||||
|| address.isMulticastAddress()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
byte[] rawAddress = address.getAddress();
|
||||
if (address instanceof Inet4Address) {
|
||||
return isPrivateOrReservedIPv4(rawAddress);
|
||||
}
|
||||
|
||||
if (address instanceof Inet6Address inet6Address) {
|
||||
if (isUniqueLocalIPv6(rawAddress)) {
|
||||
return true;
|
||||
}
|
||||
if (isIPv4MappedAddress(rawAddress) || inet6Address.isIPv4CompatibleAddress()) {
|
||||
byte[] ipv4 =
|
||||
Arrays.copyOfRange(rawAddress, rawAddress.length - 4, rawAddress.length);
|
||||
return isPrivateOrReservedIPv4(ipv4);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IPv4 address is private or reserved. Any malformed input defaults to {@code
|
||||
* true} (conservative) to avoid misuse.
|
||||
*
|
||||
* @param address 4-byte IPv4 address
|
||||
* @return {@code true} if private/reserved
|
||||
*/
|
||||
private boolean isPrivateOrReservedIPv4(byte[] address) {
|
||||
// IPv4 addresses must be exactly 4 bytes. Treat null or unexpected lengths as
|
||||
// sensitive to avoid processing malformed input.
|
||||
if (address == null || address.length != 4) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int first = Byte.toUnsignedInt(address[0]);
|
||||
int second = Byte.toUnsignedInt(address[1]);
|
||||
|
||||
if (first == 0 || first == 127) {
|
||||
return true; // 0.0.0.0/8 and 127.0.0.0/8
|
||||
}
|
||||
if (first == 100 && second >= 64 && second <= 127) {
|
||||
return true; // 100.64.0.0/10 Carrier-grade NAT
|
||||
}
|
||||
if (first == 169 && second == 254) {
|
||||
return true; // 169.254.0.0/16 Link-local
|
||||
}
|
||||
if (first == 172 && second >= 16 && second <= 31) {
|
||||
return true; // 172.16.0.0/12 Private
|
||||
}
|
||||
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 0) {
|
||||
return true; // 192.0.0.0/24 IETF Protocol Assignments
|
||||
}
|
||||
if (first == 192 && second == 0 && Byte.toUnsignedInt(address[2]) == 2) {
|
||||
return true; // 192.0.2.0/24 TEST-NET-1
|
||||
}
|
||||
if (first == 192 && second == 168) {
|
||||
return true; // 192.168.0.0/16 Private
|
||||
}
|
||||
if (first == 198 && (second == 18 || second == 19)) {
|
||||
return true; // 198.18.0.0/15 Benchmark tests
|
||||
}
|
||||
if (first == 198 && second == 51 && Byte.toUnsignedInt(address[2]) == 100) {
|
||||
return true; // 198.51.100.0/24 TEST-NET-2
|
||||
}
|
||||
if (first == 203 && second == 0 && Byte.toUnsignedInt(address[2]) == 113) {
|
||||
return true; // 203.0.113.0/24 TEST-NET-3
|
||||
}
|
||||
if (first == 10) {
|
||||
return true; // 10.0.0.0/8 Private
|
||||
}
|
||||
if (first >= 224) {
|
||||
return true; // 224.0.0.0/4 Multicast and 240.0.0.0/4 Reserved for future use
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IPv6 address is a Unique Local Address (ULA, fc00::/7). Any malformed input
|
||||
* defaults to {@code true} (conservative) to avoid misuse.
|
||||
*
|
||||
* @param address 16-byte IPv6 address
|
||||
* @return {@code true} if ULA
|
||||
*/
|
||||
private boolean isUniqueLocalIPv6(byte[] address) {
|
||||
if (address == null || address.length != 16) {
|
||||
return true;
|
||||
}
|
||||
int first = Byte.toUnsignedInt(address[0]);
|
||||
return (first & 0xFE) == 0xFC; // fc00::/7 Unique local addresses
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an IPv6 address is an IPv4-mapped address (::ffff:0:0/96). Any malformed input
|
||||
* defaults to {@code false} (conservative) to avoid misuse.
|
||||
*
|
||||
* @param address 16-byte IPv6 address
|
||||
* @return {@code true} if IPv4-mapped
|
||||
*/
|
||||
private boolean isIPv4MappedAddress(byte[] address) {
|
||||
if (address == null || address.length != 16) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (address[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return address[10] == (byte) 0xFF && address[11] == (byte) 0xFF;
|
||||
}
|
||||
|
||||
/*
|
||||
* Improved multipart file conversion using the shared helper method.
|
||||
*
|
||||
@@ -386,7 +520,7 @@ public class GeneralUtils {
|
||||
throw new IllegalArgumentException("Invalid default unit: " + defaultUnit);
|
||||
}
|
||||
|
||||
sizeStr = sizeStr.trim().toUpperCase();
|
||||
sizeStr = sizeStr.trim().toUpperCase(Locale.ROOT);
|
||||
sizeStr = sizeStr.replace(",", ".").replace(" ", "");
|
||||
|
||||
try {
|
||||
@@ -415,7 +549,7 @@ public class GeneralUtils {
|
||||
return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1));
|
||||
} else {
|
||||
// Use provided default unit or fall back to MB
|
||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase() : "MB";
|
||||
String unit = defaultUnit != null ? defaultUnit.toUpperCase(Locale.ROOT) : "MB";
|
||||
double value = Double.parseDouble(sizeStr);
|
||||
return switch (unit) {
|
||||
case "TB" -> (long) (value * 1024L * 1024L * 1024L * 1024L);
|
||||
@@ -457,13 +591,14 @@ public class GeneralUtils {
|
||||
if (bytes < 1024) {
|
||||
return bytes + " B";
|
||||
} else if (bytes < 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f KB", bytes / 1024.0);
|
||||
return String.format(Locale.ROOT, "%.2f KB", bytes / 1024.0);
|
||||
} else if (bytes < 1024L * 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
return String.format(Locale.ROOT, "%.2f MB", bytes / (1024.0 * 1024.0));
|
||||
} else if (bytes < 1024L * 1024L * 1024L * 1024L) {
|
||||
return String.format(Locale.US, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
return String.format(Locale.ROOT, "%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
|
||||
} else {
|
||||
return String.format(Locale.US, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
||||
return String.format(
|
||||
Locale.ROOT, "%.2f TB", bytes / (1024.0 * 1024.0 * 1024.0 * 1024.0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,7 +858,7 @@ public class GeneralUtils {
|
||||
byte[] mac = net.getHardwareAddress();
|
||||
if (mac != null && mac.length > 0) {
|
||||
for (byte b : mac) {
|
||||
sb.append(String.format("%02X", b));
|
||||
sb.append(String.format(Locale.ROOT, "%02X", b));
|
||||
}
|
||||
break; // Use the first valid network interface
|
||||
}
|
||||
@@ -733,7 +868,7 @@ public class GeneralUtils {
|
||||
byte[] mac = network.getHardwareAddress();
|
||||
if (mac != null) {
|
||||
for (byte b : mac) {
|
||||
sb.append(String.format("%02X", b));
|
||||
sb.append(String.format(Locale.ROOT, "%02X", b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -940,17 +1075,29 @@ public class GeneralUtils {
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
ExceptionUtils.GhostscriptException detectedError =
|
||||
ExceptionUtils.detectGhostscriptCriticalError(result.getMessages());
|
||||
if (detectedError != null) {
|
||||
log.warn(
|
||||
"Ghostscript ebook optimization reported a critical error: {}",
|
||||
detectedError.getMessage());
|
||||
throw detectedError;
|
||||
}
|
||||
|
||||
if (result.getRc() != 0) {
|
||||
log.warn(
|
||||
"Ghostscript ebook optimization failed with return code: {}",
|
||||
result.getRc());
|
||||
throw ExceptionUtils.createGhostscriptCompressionException();
|
||||
throw ExceptionUtils.createGhostscriptCompressionException(result.getMessages());
|
||||
}
|
||||
|
||||
return Files.readAllBytes(tempOutput);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("Ghostscript ebook optimization failed", e);
|
||||
if (e instanceof ExceptionUtils.GhostscriptException ghostscriptException) {
|
||||
throw ghostscriptException;
|
||||
}
|
||||
throw ExceptionUtils.createGhostscriptCompressionException(e);
|
||||
} finally {
|
||||
if (tempInput != null) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
@@ -118,7 +119,7 @@ public class ImageProcessingUtils {
|
||||
BufferedImage image = null;
|
||||
String filename = file.getOriginalFilename();
|
||||
|
||||
if (filename != null && filename.toLowerCase().endsWith(".psd")) {
|
||||
if (filename != null && filename.toLowerCase(Locale.ROOT).endsWith(".psd")) {
|
||||
// For PSD files, try explicit ImageReader
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("PSD");
|
||||
if (readers.hasNext()) {
|
||||
@@ -134,7 +135,8 @@ public class ImageProcessingUtils {
|
||||
throw new IOException(
|
||||
"Unable to read image from file: "
|
||||
+ filename
|
||||
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP compression");
|
||||
+ ". Supported PSD formats: RGB/CMYK/Gray 8-32 bit, RLE/ZIP"
|
||||
+ " compression");
|
||||
}
|
||||
} else {
|
||||
// For non-PSD files, use standard ImageIO
|
||||
@@ -142,7 +144,7 @@ public class ImageProcessingUtils {
|
||||
}
|
||||
|
||||
if (image == null) {
|
||||
throw new IOException("Unable to read image from file: " + filename);
|
||||
throw ExceptionUtils.createImageReadException(filename);
|
||||
}
|
||||
|
||||
double orientation = extractImageOrientation(file.getInputStream());
|
||||
|
||||
@@ -27,15 +27,22 @@ import io.github.pixee.security.Filenames;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
@Slf4j
|
||||
public class PDFToFile {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public PDFToFile(TempFileManager tempFileManager) {
|
||||
this(tempFileManager, null);
|
||||
}
|
||||
|
||||
public PDFToFile(TempFileManager tempFileManager, RuntimePathConfig runtimePathConfig) {
|
||||
this.tempFileManager = tempFileManager;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
public ResponseEntity<byte[]> processPdfToMarkdown(MultipartFile inputFile)
|
||||
@@ -241,31 +248,65 @@ public class PDFToFile {
|
||||
byte[] fileBytes;
|
||||
String fileName;
|
||||
|
||||
Path libreOfficeProfile = null;
|
||||
try (TempFile inputFileTemp = new TempFile(tempFileManager, ".pdf");
|
||||
TempDirectory outputDirTemp = new TempDirectory(tempFileManager)) {
|
||||
|
||||
Path tempInputFile = inputFileTemp.getPath();
|
||||
Path tempOutputDir = outputDirTemp.getPath();
|
||||
Path unoOutputFile =
|
||||
tempOutputDir.resolve(
|
||||
pdfBaseName + "." + resolvePrimaryExtension(outputFormat));
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Run the LibreOffice command
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"soffice",
|
||||
"--headless",
|
||||
"--nologo",
|
||||
"--infilter=" + libreOfficeFilter,
|
||||
"--convert-to",
|
||||
outputFormat,
|
||||
"--outdir",
|
||||
tempOutputDir.toString(),
|
||||
tempInputFile.toString()));
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
ProcessExecutorResult returnCode = null;
|
||||
IOException unoconvertException = null;
|
||||
|
||||
if (isUnoConvertEnabled()) {
|
||||
try {
|
||||
List<String> unoCommand =
|
||||
buildUnoConvertCommand(
|
||||
tempInputFile, unoOutputFile, outputFormat, libreOfficeFilter);
|
||||
returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(unoCommand);
|
||||
} catch (IOException e) {
|
||||
unoconvertException = e;
|
||||
log.warn(
|
||||
"Unoconvert command failed ({}). Falling back to soffice command.",
|
||||
e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (returnCode == null) {
|
||||
// Run the LibreOffice command as a fallback
|
||||
libreOfficeProfile = Files.createTempDirectory("libreoffice_profile_");
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getSOfficePath());
|
||||
command.add("-env:UserInstallation=" + libreOfficeProfile.toUri().toString());
|
||||
command.add("--headless");
|
||||
command.add("--nologo");
|
||||
command.add("--infilter=" + libreOfficeFilter);
|
||||
command.add("--convert-to");
|
||||
command.add(outputFormat);
|
||||
command.add("--outdir");
|
||||
command.add(tempOutputDir.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
try {
|
||||
returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (IOException e) {
|
||||
if (unoconvertException != null) {
|
||||
e.addSuppressed(unoconvertException);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Get output files
|
||||
List<File> outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles());
|
||||
@@ -300,8 +341,42 @@ public class PDFToFile {
|
||||
|
||||
fileBytes = byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
} finally {
|
||||
if (libreOfficeProfile != null) {
|
||||
FileUtils.deleteQuietly(libreOfficeProfile.toFile());
|
||||
}
|
||||
}
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
private boolean isUnoConvertEnabled() {
|
||||
return runtimePathConfig != null
|
||||
&& runtimePathConfig.getUnoConvertPath() != null
|
||||
&& !runtimePathConfig.getUnoConvertPath().isBlank();
|
||||
}
|
||||
|
||||
private List<String> buildUnoConvertCommand(
|
||||
Path inputFile, Path outputFile, String outputFormat, String libreOfficeFilter) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add(runtimePathConfig.getUnoConvertPath());
|
||||
command.add("--port");
|
||||
command.add("2003");
|
||||
command.add("--convert-to");
|
||||
command.add(outputFormat);
|
||||
if (libreOfficeFilter != null && !libreOfficeFilter.isBlank()) {
|
||||
command.add("--input-filter=" + libreOfficeFilter);
|
||||
}
|
||||
command.add(inputFile.toString());
|
||||
command.add(outputFile.toString());
|
||||
return command;
|
||||
}
|
||||
|
||||
private String resolvePrimaryExtension(String outputFormat) {
|
||||
if (outputFormat == null) {
|
||||
return "";
|
||||
}
|
||||
int colonIndex = outputFormat.indexOf(':');
|
||||
return colonIndex > 0 ? outputFormat.substring(0, colonIndex) : outputFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ public class PdfAttachmentHandler {
|
||||
|
||||
private static String normalizeFilename(String filename) {
|
||||
if (filename == null) return "";
|
||||
String normalized = filename.toLowerCase().trim();
|
||||
String normalized = filename.toLowerCase(Locale.ROOT).trim();
|
||||
normalized =
|
||||
RegexPatternUtils.getInstance()
|
||||
.getWhitespacePattern()
|
||||
@@ -560,7 +560,7 @@ public class PdfAttachmentHandler {
|
||||
@Override
|
||||
protected void writeString(String string, List<TextPosition> textPositions)
|
||||
throws IOException {
|
||||
String lowerString = string.toLowerCase();
|
||||
String lowerString = string.toLowerCase(Locale.ROOT);
|
||||
|
||||
if (ATTACHMENT_SECTION_PATTERN.matcher(lowerString).find()) {
|
||||
isInAttachmentSection = true;
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@@ -34,7 +35,7 @@ public class PdfToCbrUtils {
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException("PDF file contains no pages");
|
||||
throw ExceptionUtils.createPdfNoPages();
|
||||
}
|
||||
|
||||
return createCbrFromPdf(document, dpi);
|
||||
@@ -43,17 +44,17 @@ public class PdfToCbrUtils {
|
||||
|
||||
private static void validatePdfFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
throw ExceptionUtils.createFileNoNameException();
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
if (!"pdf".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a PDF");
|
||||
throw ExceptionUtils.createPdfFileRequiredException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,27 +67,36 @@ public class PdfToCbrUtils {
|
||||
int totalPages = document.getNumberOfPages();
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
final int currentPage = pageIndex;
|
||||
try {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||
ExceptionUtils.handleOomRendering(
|
||||
currentPage + 1,
|
||||
dpi,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
currentPage, dpi, ImageType.RGB));
|
||||
|
||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||
String imageFilename =
|
||||
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
|
||||
Path imagePath = tempDir.resolve(imageFilename);
|
||||
|
||||
ImageIO.write(image, "PNG", imagePath.toFile());
|
||||
generatedImages.add(imagePath);
|
||||
|
||||
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
|
||||
// Re-throw OOM exceptions without wrapping
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
// Wrap other IOExceptions with context
|
||||
throw ExceptionUtils.createFileProcessingException(
|
||||
"CBR creation for page " + (currentPage + 1), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (generatedImages.isEmpty()) {
|
||||
throw new IOException("Failed to render any pages to images for CBR conversion");
|
||||
throw ExceptionUtils.createFileProcessingException(
|
||||
"CBR conversion", new IOException("No pages were successfully rendered"));
|
||||
}
|
||||
|
||||
return createRarArchive(tempDir, generatedImages);
|
||||
@@ -115,15 +125,18 @@ public class PdfToCbrUtils {
|
||||
ProcessExecutorResult result =
|
||||
executor.runCommandWithOutputHandling(command, tempDir.toFile());
|
||||
if (result.getRc() != 0) {
|
||||
throw new IOException("RAR command failed: " + result.getMessages());
|
||||
throw ExceptionUtils.createFileProcessingException(
|
||||
"RAR archive creation",
|
||||
new IOException("RAR command failed with code " + result.getRc()));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("RAR command interrupted", e);
|
||||
throw ExceptionUtils.createProcessingInterruptedException("RAR creation", e);
|
||||
}
|
||||
|
||||
if (!Files.exists(rarFile)) {
|
||||
throw new IOException("RAR file was not created");
|
||||
throw ExceptionUtils.createFileProcessingException(
|
||||
"RAR archive creation", new IOException("RAR file was not created"));
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(rarFile.toFile());
|
||||
@@ -167,7 +180,7 @@ public class PdfToCbrUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
return "pdf".equals(extension);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package stirling.software.common.util;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
@@ -29,7 +30,7 @@ public class PdfToCbzUtils {
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(pdfFile)) {
|
||||
if (document.getNumberOfPages() == 0) {
|
||||
throw new IllegalArgumentException("PDF file contains no pages");
|
||||
throw ExceptionUtils.createPdfNoPages();
|
||||
}
|
||||
|
||||
return createCbzFromPdf(document, dpi);
|
||||
@@ -38,17 +39,17 @@ public class PdfToCbzUtils {
|
||||
|
||||
private static void validatePdfFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("File cannot be null or empty");
|
||||
throw ExceptionUtils.createFileNullOrEmptyException();
|
||||
}
|
||||
|
||||
String filename = file.getOriginalFilename();
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("File must have a name");
|
||||
throw ExceptionUtils.createFileNoNameException();
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
if (!"pdf".equals(extension)) {
|
||||
throw new IllegalArgumentException("File must be a PDF");
|
||||
throw ExceptionUtils.createPdfFileRequiredException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,24 +62,31 @@ public class PdfToCbzUtils {
|
||||
int totalPages = document.getNumberOfPages();
|
||||
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
final int currentPage = pageIndex;
|
||||
try {
|
||||
BufferedImage image =
|
||||
pdfRenderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
|
||||
|
||||
String imageFilename = String.format("page_%03d.png", pageIndex + 1);
|
||||
ExceptionUtils.handleOomRendering(
|
||||
currentPage + 1,
|
||||
dpi,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
currentPage, dpi, ImageType.RGB));
|
||||
|
||||
String imageFilename =
|
||||
String.format(Locale.ROOT, "page_%03d.png", currentPage + 1);
|
||||
ZipEntry zipEntry = new ZipEntry(imageFilename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
|
||||
ImageIO.write(image, "PNG", zipOut);
|
||||
zipOut.closeEntry();
|
||||
|
||||
} catch (ExceptionUtils.OutOfMemoryDpiException e) {
|
||||
// Re-throw OOM exceptions without wrapping
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
log.warn("Error processing page {}: {}", pageIndex + 1, e.getMessage());
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(pageIndex + 1, dpi, e);
|
||||
// Wrap other IOExceptions with context
|
||||
throw ExceptionUtils.createFileProcessingException(
|
||||
"CBZ creation for page " + (currentPage + 1), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +101,7 @@ public class PdfToCbzUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase();
|
||||
String extension = FilenameUtils.getExtension(filename).toLowerCase(Locale.ROOT);
|
||||
return "pdf".equals(extension);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
@@ -50,36 +51,18 @@ public class PdfUtils {
|
||||
|
||||
public PDRectangle textToPageSize(String size) {
|
||||
|
||||
switch (size.toUpperCase()) {
|
||||
case "A0" -> {
|
||||
return PDRectangle.A0;
|
||||
}
|
||||
case "A1" -> {
|
||||
return PDRectangle.A1;
|
||||
}
|
||||
case "A2" -> {
|
||||
return PDRectangle.A2;
|
||||
}
|
||||
case "A3" -> {
|
||||
return PDRectangle.A3;
|
||||
}
|
||||
case "A4" -> {
|
||||
return PDRectangle.A4;
|
||||
}
|
||||
case "A5" -> {
|
||||
return PDRectangle.A5;
|
||||
}
|
||||
case "A6" -> {
|
||||
return PDRectangle.A6;
|
||||
}
|
||||
case "LETTER" -> {
|
||||
return PDRectangle.LETTER;
|
||||
}
|
||||
case "LEGAL" -> {
|
||||
return PDRectangle.LEGAL;
|
||||
}
|
||||
return switch (size.toUpperCase(Locale.ROOT)) {
|
||||
case "A0" -> PDRectangle.A0;
|
||||
case "A1" -> PDRectangle.A1;
|
||||
case "A2" -> PDRectangle.A2;
|
||||
case "A3" -> PDRectangle.A3;
|
||||
case "A4" -> PDRectangle.A4;
|
||||
case "A5" -> PDRectangle.A5;
|
||||
case "A6" -> PDRectangle.A6;
|
||||
case "LETTER" -> PDRectangle.LETTER;
|
||||
case "LEGAL" -> PDRectangle.LEGAL;
|
||||
default -> throw ExceptionUtils.createInvalidPageSizeException(size);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public List<RenderedImage> getAllImages(PDResources resources) throws IOException {
|
||||
@@ -182,8 +165,8 @@ public class PdfUtils {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
if (singleImage) {
|
||||
if ("tiff".equals(imageType.toLowerCase())
|
||||
|| "tif".equals(imageType.toLowerCase())) {
|
||||
if ("tiff".equals(imageType.toLowerCase(Locale.ROOT))
|
||||
|| "tif".equals(imageType.toLowerCase(Locale.ROOT))) {
|
||||
// Write the images to the output stream as a TIFF with multiple frames
|
||||
ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next();
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
@@ -196,9 +179,20 @@ public class PdfUtils {
|
||||
writer.prepareWriteSequence(null);
|
||||
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
final int pageIndex = i;
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
// Validate dimensions before rendering
|
||||
ExceptionUtils.validateRenderingDimensions(
|
||||
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||
|
||||
image =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageIndex + 1,
|
||||
DPI,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
pageIndex, DPI, colorType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
@@ -212,10 +206,6 @@ public class PdfUtils {
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
writer.writeToSequence(new IIOImage(image, null, null), param);
|
||||
}
|
||||
@@ -239,6 +229,7 @@ public class PdfUtils {
|
||||
HashMap<PdfRenderSettingsKey, PdfImageDimensionValue> pageSizes =
|
||||
new HashMap<>();
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
final int pageIndex = i;
|
||||
PDPage page = document.getPage(i);
|
||||
PDRectangle mediaBox = page.getMediaBox();
|
||||
int rotation = page.getRotation();
|
||||
@@ -249,7 +240,17 @@ public class PdfUtils {
|
||||
if (dimension == null) {
|
||||
// Render the image to get the dimensions
|
||||
try {
|
||||
pdfSizeImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
// Validate dimensions before rendering
|
||||
ExceptionUtils.validateRenderingDimensions(
|
||||
page, pageIndex + 1, DPI);
|
||||
|
||||
pdfSizeImage =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageIndex + 1,
|
||||
DPI,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
pageIndex, DPI, colorType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
@@ -264,10 +265,6 @@ public class PdfUtils {
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
pdfSizeImageIndex = i;
|
||||
dimension =
|
||||
@@ -293,11 +290,22 @@ public class PdfUtils {
|
||||
boolean firstImageAlreadyRendered = pdfSizeImageIndex == 0;
|
||||
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
final int pageIndex = i;
|
||||
if (firstImageAlreadyRendered && i == 0) {
|
||||
pageImage = pdfSizeImage;
|
||||
} else {
|
||||
try {
|
||||
pageImage = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
// Validate dimensions before rendering
|
||||
ExceptionUtils.validateRenderingDimensions(
|
||||
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||
|
||||
pageImage =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageIndex + 1,
|
||||
DPI,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
pageIndex, DPI, colorType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage()
|
||||
@@ -311,10 +319,6 @@ public class PdfUtils {
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,9 +339,20 @@ public class PdfUtils {
|
||||
// Zip the images and return as byte array
|
||||
try (ZipOutputStream zos = new ZipOutputStream(baos)) {
|
||||
for (int i = 0; i < pageCount; ++i) {
|
||||
final int pageIndex = i;
|
||||
BufferedImage image;
|
||||
try {
|
||||
image = pdfRenderer.renderImageWithDPI(i, DPI, colorType);
|
||||
// Validate dimensions before rendering
|
||||
ExceptionUtils.validateRenderingDimensions(
|
||||
document.getPage(pageIndex), pageIndex + 1, DPI);
|
||||
|
||||
image =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageIndex + 1,
|
||||
DPI,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
pageIndex, DPI, colorType));
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||
@@ -349,10 +364,6 @@ public class PdfUtils {
|
||||
DPI);
|
||||
}
|
||||
throw e;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(i + 1, DPI, e);
|
||||
}
|
||||
try (ByteArrayOutputStream baosImage = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, imageType, baosImage);
|
||||
@@ -361,9 +372,10 @@ public class PdfUtils {
|
||||
zos.putNextEntry(
|
||||
new ZipEntry(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
filename + "_%d.%s",
|
||||
i + 1,
|
||||
imageType.toLowerCase())));
|
||||
imageType.toLowerCase(Locale.ROOT))));
|
||||
zos.write(baosImage.toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -391,6 +403,7 @@ public class PdfUtils {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
pdfRenderer.setSubsamplingAllowed(true);
|
||||
for (int page = 0; page < document.getNumberOfPages(); ++page) {
|
||||
final int pageIndex = page;
|
||||
BufferedImage bim;
|
||||
|
||||
// Use global maximum DPI setting, fallback to 300 if not set
|
||||
@@ -400,9 +413,16 @@ public class PdfUtils {
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
renderDpi = properties.getSystem().getMaxDPI();
|
||||
}
|
||||
final int dpi = renderDpi;
|
||||
|
||||
try {
|
||||
bim = pdfRenderer.renderImageWithDPI(page, renderDpi, ImageType.RGB);
|
||||
bim =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageIndex + 1,
|
||||
dpi,
|
||||
() ->
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
pageIndex, dpi, ImageType.RGB));
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (e.getMessage() != null
|
||||
&& e.getMessage().contains("Maximum size of image exceeded")) {
|
||||
@@ -411,13 +431,9 @@ public class PdfUtils {
|
||||
"PDF page {0} is too large to render at 300 DPI. The resulting image"
|
||||
+ " would exceed Java's maximum array size. Please use a lower DPI"
|
||||
+ " value for PDF-to-image conversion.",
|
||||
page + 1);
|
||||
pageIndex + 1);
|
||||
}
|
||||
throw e;
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, 300, e);
|
||||
}
|
||||
PDPage originalPage = document.getPage(page);
|
||||
|
||||
@@ -463,8 +479,8 @@ public class PdfUtils {
|
||||
String contentType = file.getContentType();
|
||||
String originalFilename = Filenames.toSimpleFileName(file.getOriginalFilename());
|
||||
if (originalFilename != null
|
||||
&& (originalFilename.toLowerCase().endsWith(".tiff")
|
||||
|| originalFilename.toLowerCase().endsWith(".tif"))) {
|
||||
&& (originalFilename.toLowerCase(Locale.ROOT).endsWith(".tiff")
|
||||
|| originalFilename.toLowerCase(Locale.ROOT).endsWith(".tif"))) {
|
||||
ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next();
|
||||
reader.setInput(ImageIO.createImageInputStream(file.getInputStream()));
|
||||
int numPages = reader.getNumImages(true);
|
||||
@@ -567,7 +583,7 @@ public class PdfUtils {
|
||||
PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, "");
|
||||
// Draw the image onto the page at the specified x and y coordinates
|
||||
contentStream.drawImage(image, x, y);
|
||||
log.info("Image successfully overlayed onto PDF");
|
||||
log.info("Image successfully overlaid onto PDF");
|
||||
if (!everyPage && i == 0) {
|
||||
break;
|
||||
}
|
||||
@@ -631,7 +647,7 @@ public class PdfUtils {
|
||||
int actualPageCount = pdfDocument.getNumberOfPages();
|
||||
pdfDocument.close();
|
||||
|
||||
return switch (comparator.toLowerCase()) {
|
||||
return switch (comparator.toLowerCase(Locale.ROOT)) {
|
||||
case "greater" -> actualPageCount > pageCount;
|
||||
case "equal" -> actualPageCount == pageCount;
|
||||
case "less" -> actualPageCount < pageCount;
|
||||
|
||||
@@ -86,6 +86,11 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getCalibreSessionLimit();
|
||||
case IMAGEMAGICK ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getImageMagickSessionLimit();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -97,6 +102,11 @@ public class ProcessExecutor {
|
||||
.getSessionLimit()
|
||||
.getOcrMyPdfSessionLimit();
|
||||
case CFF_CONVERTER -> 1;
|
||||
case FFMPEG ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getFfmpegSessionLimit();
|
||||
};
|
||||
|
||||
long timeoutMinutes =
|
||||
@@ -141,6 +151,11 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getCalibreTimeoutMinutes();
|
||||
case IMAGEMAGICK ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getImageMagickTimeoutMinutes();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -152,6 +167,11 @@ public class ProcessExecutor {
|
||||
.getTimeoutMinutes()
|
||||
.getOcrMyPdfTimeoutMinutes();
|
||||
case CFF_CONVERTER -> 5L;
|
||||
case FFMPEG ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getFfmpegTimeoutMinutes();
|
||||
};
|
||||
return new ProcessExecutor(semaphoreLimit, liveUpdates, timeoutMinutes);
|
||||
});
|
||||
@@ -301,11 +321,13 @@ public class ProcessExecutor {
|
||||
WEASYPRINT,
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
IMAGEMAGICK,
|
||||
TESSERACT,
|
||||
QPDF,
|
||||
GHOSTSCRIPT,
|
||||
OCR_MY_PDF,
|
||||
CFF_CONVERTER
|
||||
CFF_CONVERTER,
|
||||
FFMPEG
|
||||
}
|
||||
|
||||
@Setter
|
||||
|
||||
@@ -325,6 +325,11 @@ public final class RegexPatternUtils {
|
||||
return getPattern("\\+");
|
||||
}
|
||||
|
||||
/** Pattern for splitting on pipe delimiter (used for hint lists in i18n messages) */
|
||||
public Pattern getPipeDelimiterPattern() {
|
||||
return getPattern("\\|");
|
||||
}
|
||||
|
||||
/** Pattern for username validation */
|
||||
public Pattern getUsernameValidationPattern() {
|
||||
return getPattern("^[a-zA-Z0-9](?!.*[-@._+]{2,})[a-zA-Z0-9@._+-]{1,48}[a-zA-Z0-9]$");
|
||||
@@ -567,6 +572,16 @@ public final class RegexPatternUtils {
|
||||
return getPattern("^[a-zA-Z0-9]{2,4}$", Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
|
||||
/** Pattern for splitting on line breaks (Unicode line separator) */
|
||||
public Pattern getLineSeparatorPattern() {
|
||||
return getPattern("\\R");
|
||||
}
|
||||
|
||||
/** Pattern for removing leading asterisks and whitespace */
|
||||
public Pattern getLeadingAsterisksWhitespacePattern() {
|
||||
return getPattern("^[*\\s]+");
|
||||
}
|
||||
|
||||
private record PatternKey(String regex, int flags) {
|
||||
// Record automatically provides equals, hashCode, and toString
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public class RequestUriUtils {
|
||||
|| normalizedUri.startsWith("/public/")
|
||||
|| normalizedUri.startsWith("/pdfjs/")
|
||||
|| normalizedUri.startsWith("/pdfjs-legacy/")
|
||||
|| normalizedUri.startsWith("/pdfium/")
|
||||
|| normalizedUri.startsWith("/assets/")
|
||||
|| normalizedUri.startsWith("/locales/")
|
||||
|| normalizedUri.startsWith("/Login/")
|
||||
@@ -39,6 +40,7 @@ public class RequestUriUtils {
|
||||
// Specific static files bundled with the frontend
|
||||
if (normalizedUri.equals("/robots.txt")
|
||||
|| normalizedUri.equals("/favicon.ico")
|
||||
|| normalizedUri.equals("/manifest.json")
|
||||
|| normalizedUri.equals("/site.webmanifest")
|
||||
|| normalizedUri.equals("/manifest-classic.json")
|
||||
|| normalizedUri.equals("/index.html")) {
|
||||
@@ -60,7 +62,8 @@ public class RequestUriUtils {
|
||||
|| normalizedUri.endsWith(".css")
|
||||
|| normalizedUri.endsWith(".mjs")
|
||||
|| normalizedUri.endsWith(".html")
|
||||
|| normalizedUri.endsWith(".toml");
|
||||
|| normalizedUri.endsWith(".toml")
|
||||
|| normalizedUri.endsWith(".wasm");
|
||||
}
|
||||
|
||||
public static boolean isFrontendRoute(String contextPath, String requestURI) {
|
||||
@@ -124,11 +127,13 @@ public class RequestUriUtils {
|
||||
|| requestURI.endsWith("popularity.txt")
|
||||
|| requestURI.endsWith(".js")
|
||||
|| requestURI.endsWith(".toml")
|
||||
|| requestURI.endsWith(".wasm")
|
||||
|| requestURI.contains("swagger")
|
||||
|| requestURI.startsWith("/api/v1/info")
|
||||
|| requestURI.startsWith("/site.webmanifest")
|
||||
|| requestURI.startsWith("/fonts")
|
||||
|| requestURI.startsWith("/pdfjs"));
|
||||
|| requestURI.startsWith("/pdfjs")
|
||||
|| requestURI.startsWith("/pdfium"));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,10 +166,9 @@ public class RequestUriUtils {
|
||||
// enableLogin)
|
||||
|| trimmedUri.startsWith(
|
||||
"/api/v1/ui-data/footer-info") // Public footer configuration
|
||||
|| trimmedUri.startsWith("/v1/api-docs")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/validate")
|
||||
|| trimmedUri.startsWith("/api/v1/invite/accept")
|
||||
|| trimmedUri.contains("/v1/api-docs");
|
||||
|| trimmedUri.startsWith("/v1/api-docs");
|
||||
}
|
||||
|
||||
private static String stripContextPath(String contextPath, String requestURI) {
|
||||
|
||||
@@ -28,6 +28,17 @@ public class TempFileManager {
|
||||
private final TempFileRegistry registry;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
/**
|
||||
* Create a managed temporary file that will be tracked by the TempFileManager.
|
||||
*
|
||||
* @param suffix The suffix for the temporary file
|
||||
* @return The created temporary file wrapper
|
||||
* @throws IOException If an I/O error occurs
|
||||
*/
|
||||
public TempFile createManagedTempFile(String suffix) throws IOException {
|
||||
return new TempFile(this, suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temporary file with the Stirling-PDF prefix. The file is automatically registered
|
||||
* with the registry.
|
||||
@@ -130,7 +141,6 @@ public class TempFileManager {
|
||||
return deleted;
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to delete temp file: {}", path.toString(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class ValidationUtil {
|
||||
|
||||
public static boolean isStringEmpty(String input) {
|
||||
return input == null || input.isBlank();
|
||||
}
|
||||
|
||||
public static boolean isCollectionEmpty(Collection<String> input) {
|
||||
return input == null || input.isEmpty();
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -56,16 +56,14 @@ public class InvertFullColorStrategy extends ReplaceAndInvertColorStrategy {
|
||||
if (properties != null && properties.getSystem() != null) {
|
||||
renderDpi = properties.getSystem().getMaxDPI();
|
||||
}
|
||||
final int dpi = renderDpi;
|
||||
final int pageNum = page;
|
||||
|
||||
try {
|
||||
image =
|
||||
pdfRenderer.renderImageWithDPI(
|
||||
page, renderDpi); // Render page with global DPI setting
|
||||
} catch (OutOfMemoryError e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||
} catch (NegativeArraySizeException e) {
|
||||
throw ExceptionUtils.createOutOfMemoryDpiException(page + 1, renderDpi, e);
|
||||
}
|
||||
image =
|
||||
ExceptionUtils.handleOomRendering(
|
||||
pageNum + 1,
|
||||
dpi,
|
||||
() -> pdfRenderer.renderImageWithDPI(pageNum, dpi));
|
||||
|
||||
// Invert the colors
|
||||
invertImageColors(image);
|
||||
|
||||
+3
-11
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.annotations;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
@@ -31,8 +29,6 @@ import stirling.software.common.aop.AutoJobAspect;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.FileStorage;
|
||||
import stirling.software.common.service.JobExecutorService;
|
||||
import stirling.software.common.service.JobQueue;
|
||||
import stirling.software.common.service.ResourceMonitor;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AutoJobPostMappingIntegrationTest {
|
||||
@@ -45,10 +41,6 @@ class AutoJobPostMappingIntegrationTest {
|
||||
|
||||
@Mock private FileStorage fileStorage;
|
||||
|
||||
@Mock private ResourceMonitor resourceMonitor;
|
||||
|
||||
@Mock private JobQueue jobQueue;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
autoJobAspect = new AutoJobAspect(jobExecutorService, request, fileStorage);
|
||||
@@ -73,7 +65,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
// Given
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileId("test-file-id");
|
||||
Object[] args = new Object[] {pdfFile};
|
||||
Object[] args = {pdfFile};
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(args);
|
||||
when(request.getParameter("async")).thenReturn("true");
|
||||
@@ -153,7 +145,7 @@ class AutoJobPostMappingIntegrationTest {
|
||||
// Given
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(mock(MultipartFile.class));
|
||||
Object[] args = new Object[] {pdfFile};
|
||||
Object[] args = {pdfFile};
|
||||
|
||||
when(joinPoint.getArgs()).thenReturn(args);
|
||||
when(request.getParameter("async")).thenReturn("true");
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package stirling.software.common.configuration.interfaces;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class ShowAdminInterfaceTest {
|
||||
|
||||
// Create a simple implementation for testing
|
||||
static class TestImpl implements ShowAdminInterface {}
|
||||
|
||||
@Test
|
||||
void getShowUpdateOnlyAdmins_returnsTrueByDefault() {
|
||||
ShowAdminInterface instance = new TestImpl();
|
||||
assertTrue(instance.getShowUpdateOnlyAdmins(), "Default should return true");
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -18,17 +18,17 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
||||
|
||||
@Test
|
||||
void loads_yaml_into_environment() throws Exception {
|
||||
// YAML-Config in Temp-Datei schreiben
|
||||
String yaml =
|
||||
""
|
||||
+ "ui:\n"
|
||||
+ " appName: \"My App\"\n"
|
||||
+ "system:\n"
|
||||
+ " enableAnalytics: true\n";
|
||||
"""
|
||||
\
|
||||
ui:
|
||||
appName: "My App"
|
||||
system:
|
||||
enableAnalytics: true
|
||||
""";
|
||||
Path tmp = Files.createTempFile("spdf-settings-", ".yml");
|
||||
Files.writeString(tmp, yaml);
|
||||
|
||||
// Pfad per statischem Mock liefern
|
||||
try (MockedStatic<InstallationPathConfig> mocked =
|
||||
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||
mocked.when(InstallationPathConfig::getSettingsPath).thenReturn(tmp.toString());
|
||||
@@ -36,7 +36,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
||||
ConfigurableEnvironment env = new StandardEnvironment();
|
||||
ApplicationProperties props = new ApplicationProperties();
|
||||
|
||||
props.dynamicYamlPropertySource(env); // fügt PropertySource an erster Stelle ein
|
||||
props.dynamicYamlPropertySource(env);
|
||||
|
||||
assertEquals("My App", env.getProperty("ui.appName"));
|
||||
assertEquals("true", env.getProperty("system.enableAnalytics"));
|
||||
@@ -44,7 +44,7 @@ class ApplicationPropertiesDynamicYamlPropertySourceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void throws_when_settings_file_missing() throws Exception {
|
||||
void throws_when_settings_file_missing() {
|
||||
String missing = "/path/does/not/exist/spdf.yml";
|
||||
try (MockedStatic<InstallationPathConfig> mocked =
|
||||
Mockito.mockStatic(InstallationPathConfig.class)) {
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
Resource r = s.getSpCert();
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
||||
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||
assertTrue(r.exists(), "Temp file should exist");
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ class ApplicationPropertiesSaml2HttpTest {
|
||||
Resource r = s.getIdpCert();
|
||||
|
||||
assertNotNull(r);
|
||||
assertTrue(r instanceof FileSystemResource, "Expected FileSystemResource for FS path");
|
||||
assertInstanceOf(FileSystemResource.class, r, "Expected FileSystemResource for FS path");
|
||||
assertFalse(r.exists(), "Resource should not exist for missing file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
|
||||
public class FileInfoTest {
|
||||
|
||||
private static final LocalDateTime FIXED_NOW = LocalDateTime.of(2025, 11, 1, 12, 0, 0);
|
||||
|
||||
@ParameterizedTest(name = "{index}: fileSize={0}")
|
||||
@CsvSource({
|
||||
"0, '0 Bytes'",
|
||||
@@ -25,87 +29,146 @@ public class FileInfoTest {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
File.separator
|
||||
+ "path"
|
||||
+ File.separator
|
||||
+ "to"
|
||||
+ File.separator
|
||||
+ "example.txt",
|
||||
LocalDateTime.now(),
|
||||
"/path/to/example.txt",
|
||||
FIXED_NOW,
|
||||
fileSize,
|
||||
LocalDateTime.now().minusDays(1));
|
||||
FIXED_NOW.minusDays(1));
|
||||
|
||||
assertEquals(expectedFormattedSize, fileInfo.getFormattedFileSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFilePathAsPath() {
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"test.pdf",
|
||||
File.separator + "tmp" + File.separator + "test.pdf",
|
||||
LocalDateTime.now(),
|
||||
1234,
|
||||
LocalDateTime.now().minusDays(2));
|
||||
assertEquals(
|
||||
File.separator + "tmp" + File.separator + "test.pdf",
|
||||
fileInfo.getFilePathAsPath().toString());
|
||||
@Nested
|
||||
@DisplayName("getFilePathAsPath")
|
||||
class GetFilePathAsPathTests {
|
||||
@Test
|
||||
@DisplayName("Should convert filePath string into a Path instance")
|
||||
void shouldConvertStringToPath() {
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
FIXED_NOW,
|
||||
123,
|
||||
FIXED_NOW.minusDays(1));
|
||||
|
||||
Path path = fi.getFilePathAsPath();
|
||||
|
||||
// Basic sanity checks
|
||||
assertNotNull(path, "Path should not be null");
|
||||
assertEquals(
|
||||
Path.of("/path/to/example.txt"),
|
||||
path,
|
||||
"Converted Path should match input string");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFormattedModificationDate() {
|
||||
LocalDateTime modDate = LocalDateTime.of(2024, 6, 1, 15, 30, 45);
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"file.txt",
|
||||
File.separator + "file.txt",
|
||||
modDate,
|
||||
100,
|
||||
LocalDateTime.of(2024, 5, 31, 10, 0, 0));
|
||||
assertEquals("2024-06-01 15:30:45", fileInfo.getFormattedModificationDate());
|
||||
@Nested
|
||||
@DisplayName("Date formatting")
|
||||
class DateFormattingTests {
|
||||
@Test
|
||||
@DisplayName("Should format modificationDate as 'yyyy-MM-dd HH:mm:ss'")
|
||||
void shouldFormatModificationDate() {
|
||||
LocalDateTime mod = LocalDateTime.of(2025, 8, 10, 15, 30, 45);
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
mod,
|
||||
1,
|
||||
LocalDateTime.of(2024, 1, 1, 0, 0, 0));
|
||||
|
||||
assertEquals("2025-08-10 15:30:45", fi.getFormattedModificationDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should format creationDate as 'yyyy-MM-dd HH:mm:ss'")
|
||||
void shouldFormatCreationDate() {
|
||||
LocalDateTime created = LocalDateTime.of(2024, 12, 31, 23, 59, 59);
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
LocalDateTime.of(2025, 1, 1, 0, 0, 0),
|
||||
1,
|
||||
created);
|
||||
|
||||
assertEquals("2024-12-31 23:59:59", fi.getFormattedCreationDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw NPE when modificationDate is null (current behavior)")
|
||||
void shouldThrowWhenModificationDateNull() {
|
||||
// Assumption: Current implementation does not guard null -> NPE is expected.
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
null, // modificationDate null
|
||||
1,
|
||||
FIXED_NOW);
|
||||
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
fi::getFormattedModificationDate,
|
||||
"Formatting a null modificationDate should throw NPE with current"
|
||||
+ " implementation");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should throw NPE when creationDate is null (current behavior)")
|
||||
void shouldThrowWhenCreationDateNull() {
|
||||
// Assumption: Current implementation does not guard null -> NPE is expected.
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
FIXED_NOW,
|
||||
1,
|
||||
null); // creationDate null
|
||||
|
||||
assertThrows(
|
||||
NullPointerException.class,
|
||||
fi::getFormattedCreationDate,
|
||||
"Formatting a null creationDate should throw NPE with current implementation");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetFormattedCreationDate() {
|
||||
LocalDateTime creationDate = LocalDateTime.of(2023, 12, 25, 8, 15, 0);
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"holiday.txt",
|
||||
File.separator + "holiday.txt",
|
||||
LocalDateTime.of(2024, 1, 1, 0, 0, 0),
|
||||
500,
|
||||
creationDate);
|
||||
assertEquals("2023-12-25 08:15:00", fileInfo.getFormattedCreationDate());
|
||||
}
|
||||
@Nested
|
||||
@DisplayName("Additional size formatting cases")
|
||||
class AdditionalSizeFormattingTests {
|
||||
|
||||
@Test
|
||||
void testGettersAndSetters() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
FileInfo fileInfo =
|
||||
new FileInfo(
|
||||
"doc.pdf",
|
||||
File.separator + "docs" + File.separator + "doc.pdf",
|
||||
now,
|
||||
2048,
|
||||
now.minusDays(1));
|
||||
// Test getters
|
||||
assertEquals("doc.pdf", fileInfo.getFileName());
|
||||
assertEquals(File.separator + "docs" + File.separator + "doc.pdf", fileInfo.getFilePath());
|
||||
assertEquals(now, fileInfo.getModificationDate());
|
||||
assertEquals(2048, fileInfo.getFileSize());
|
||||
assertEquals(now.minusDays(1), fileInfo.getCreationDate());
|
||||
@Test
|
||||
@DisplayName("Should round to two decimals for KB (e.g., 1536 B -> 1.50 KB)")
|
||||
void shouldRoundKbToTwoDecimals() {
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
FIXED_NOW,
|
||||
1536, // 1.5 KB
|
||||
FIXED_NOW.minusDays(1));
|
||||
|
||||
// Test setters
|
||||
fileInfo.setFileName("new.pdf");
|
||||
fileInfo.setFilePath(File.separator + "new" + File.separator + "new.pdf");
|
||||
fileInfo.setModificationDate(now.plusDays(1));
|
||||
fileInfo.setFileSize(4096);
|
||||
fileInfo.setCreationDate(now.minusDays(2));
|
||||
assertEquals("1.50 KB", fi.getFormattedFileSize());
|
||||
}
|
||||
|
||||
assertEquals("new.pdf", fileInfo.getFileName());
|
||||
assertEquals(File.separator + "new" + File.separator + "new.pdf", fileInfo.getFilePath());
|
||||
assertEquals(now.plusDays(1), fileInfo.getModificationDate());
|
||||
assertEquals(4096, fileInfo.getFileSize());
|
||||
assertEquals(now.minusDays(2), fileInfo.getCreationDate());
|
||||
@Test
|
||||
@DisplayName("Values above 1 TB are still represented in GB (design choice)")
|
||||
void shouldRepresentTerabytesInGb() {
|
||||
// 2 TB = 2 * 1024 GB -> 2 * 1024 * 1024^3 bytes
|
||||
long twoTB = 2L * 1024 * 1024 * 1024 * 1024; // 2 * 2^40
|
||||
FileInfo fi =
|
||||
new FileInfo(
|
||||
"example.txt",
|
||||
"/path/to/example.txt",
|
||||
FIXED_NOW,
|
||||
twoTB,
|
||||
FIXED_NOW.minusDays(1));
|
||||
|
||||
// 2 TB equals 2048.00 GB with current implementation
|
||||
assertEquals(
|
||||
"2048.00 GB",
|
||||
fi.getFormattedFileSize(),
|
||||
"Current implementation caps at GB and shows TB in GB units");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package stirling.software.common.model.exception;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@DisplayName("Tests for UnsupportedClaimException")
|
||||
class UnsupportedClaimExceptionTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("should store message passed to constructor")
|
||||
void shouldStoreMessageFromConstructor() {
|
||||
String expectedMessage = "This claim is not supported";
|
||||
UnsupportedClaimException exception = new UnsupportedClaimException(expectedMessage);
|
||||
|
||||
// Verify the stored message
|
||||
assertEquals(
|
||||
expectedMessage,
|
||||
exception.getMessage(),
|
||||
"Constructor should correctly store the provided message");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should allow null message without throwing exception")
|
||||
void shouldAllowNullMessage() {
|
||||
UnsupportedClaimException exception = new UnsupportedClaimException(null);
|
||||
|
||||
// Null message should be stored as null
|
||||
assertNull(
|
||||
exception.getMessage(),
|
||||
"Constructor should accept null message and store it as null");
|
||||
}
|
||||
}
|
||||
+52
-59
@@ -1,7 +1,6 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static stirling.software.common.service.SpyPDFDocumentFactory.*;
|
||||
|
||||
@@ -22,7 +21,6 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.SpyPDFDocumentFactory.StrategyType;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
@@ -43,58 +41,7 @@ class CustomPDFDocumentFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||
try (PDDocument doc = factory.load(file)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
try (PDDocument doc = factory.load(inflated)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
try (PDDocument doc = factory.load(multipart)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(multipart);
|
||||
try (PDDocument doc = factory.load(pdfFile)) {
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
|
||||
private static byte[] inflatePdf(byte[] input, int sizeInMB) throws IOException {
|
||||
try (PDDocument doc = Loader.loadPDF(input)) {
|
||||
byte[] largeData = new byte[sizeInMB * 1024 * 1024];
|
||||
Arrays.fill(largeData, (byte) 'A');
|
||||
@@ -113,6 +60,46 @@ class CustomPDFDocumentFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
private static File writeTempFile(byte[] content) throws IOException {
|
||||
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
|
||||
Files.write(file.toPath(), content);
|
||||
return file;
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||
factory.load(file);
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
factory.load(inflated);
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
factory.load(new ByteArrayInputStream(inflated));
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
factory.load(multipart);
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLoadFromPath() throws IOException {
|
||||
File file = writeTempFile(inflatePdf(basePdfBytes, 5));
|
||||
@@ -130,7 +117,7 @@ class CustomPDFDocumentFactoryTest {
|
||||
}
|
||||
}
|
||||
|
||||
// neeed to add password pdf
|
||||
// need to add password pdf
|
||||
// @Test
|
||||
// void testLoadPasswordProtectedPdfFromInputStream() throws IOException {
|
||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||
@@ -212,10 +199,16 @@ class CustomPDFDocumentFactoryTest {
|
||||
assertTrue(newBytes.length > 0);
|
||||
}
|
||||
|
||||
private File writeTempFile(byte[] content) throws IOException {
|
||||
File file = Files.createTempFile("pdf-test-", ".pdf").toFile();
|
||||
Files.write(file.toPath(), content);
|
||||
return file;
|
||||
@ParameterizedTest
|
||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||
MockMultipartFile multipart =
|
||||
new MockMultipartFile("file", "doc.pdf", MediaType.APPLICATION_PDF_VALUE, inflated);
|
||||
PDFFile pdfFile = new PDFFile();
|
||||
pdfFile.setFileInput(multipart);
|
||||
factory.load(pdfFile);
|
||||
Assertions.assertEquals(expected, factory.lastStrategyUsed);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.AdditionalAnswers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -86,7 +82,7 @@ class FileStorageTest {
|
||||
void testRetrieveFile() throws IOException {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
String fileId = "test-file-1";
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
@@ -106,7 +102,7 @@ class FileStorageTest {
|
||||
void testRetrieveBytes() throws IOException {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
String fileId = "test-file-2";
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
@@ -139,7 +135,7 @@ class FileStorageTest {
|
||||
void testDeleteFile() throws IOException {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
String fileId = "test-file-3";
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
@@ -167,7 +163,7 @@ class FileStorageTest {
|
||||
void testFileExists() throws IOException {
|
||||
// Arrange
|
||||
byte[] fileContent = "Test PDF content".getBytes();
|
||||
String fileId = UUID.randomUUID().toString();
|
||||
String fileId = "test-file-4";
|
||||
Path filePath = tempDir.resolve(fileId);
|
||||
Files.write(filePath, fileContent);
|
||||
|
||||
|
||||
+11
-10
@@ -1,6 +1,7 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -105,7 +106,7 @@ class JobExecutorServiceTest {
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertTrue(response.getBody() instanceof JobResponse);
|
||||
assertInstanceOf(JobResponse.class, response.getBody());
|
||||
JobResponse<?> jobResponse = (JobResponse<?>) response.getBody();
|
||||
assertTrue(jobResponse.isAsync());
|
||||
assertNotNull(jobResponse.getJobId());
|
||||
@@ -134,7 +135,7 @@ class JobExecutorServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldQueueJobWhenResourcesLimited() {
|
||||
void shouldQueueJobWhenResourcesLimited() throws Exception {
|
||||
// Given
|
||||
Supplier<Object> work = () -> "test-result";
|
||||
CompletableFuture<ResponseEntity<?>> future = new CompletableFuture<>();
|
||||
@@ -150,7 +151,7 @@ class JobExecutorServiceTest {
|
||||
|
||||
// Then
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertTrue(response.getBody() instanceof JobResponse);
|
||||
assertInstanceOf(JobResponse.class, response.getBody());
|
||||
|
||||
// Verify job was queued
|
||||
verify(jobQueue).queueJob(anyString(), eq(80), any(), eq(5000L));
|
||||
@@ -184,13 +185,13 @@ class JobExecutorServiceTest {
|
||||
// Given
|
||||
Supplier<Object> work =
|
||||
() -> {
|
||||
try {
|
||||
Thread.sleep(100); // Simulate long-running job
|
||||
return "test-result";
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(e);
|
||||
// Simulate long-running job without actual sleep
|
||||
// Use a loop to consume time instead of Thread.sleep
|
||||
long startTime = System.nanoTime();
|
||||
while (System.nanoTime() - startTime < 100_000_000) { // 100ms in nanoseconds
|
||||
// Busy wait to simulate work without Thread.sleep
|
||||
}
|
||||
return "test-result";
|
||||
};
|
||||
|
||||
// Use reflection to access the private executeWithTimeout method
|
||||
@@ -203,7 +204,7 @@ class JobExecutorServiceTest {
|
||||
try {
|
||||
executeMethod.invoke(jobExecutorService, work, 1L); // Very short timeout
|
||||
} catch (Exception e) {
|
||||
assertTrue(e.getCause() instanceof TimeoutException);
|
||||
assertInstanceOf(TimeoutException.class, e.getCause());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.OperatingSystemMXBean;
|
||||
import java.time.Instant;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -33,11 +32,11 @@ class ResourceMonitorTest {
|
||||
@Mock private MemoryMXBean memoryMXBean;
|
||||
|
||||
@Spy
|
||||
private AtomicReference<ResourceStatus> currentStatus =
|
||||
private final AtomicReference<ResourceStatus> currentStatus =
|
||||
new AtomicReference<>(ResourceStatus.OK);
|
||||
|
||||
@Spy
|
||||
private AtomicReference<ResourceMetrics> latestMetrics =
|
||||
private final AtomicReference<ResourceMetrics> latestMetrics =
|
||||
new AtomicReference<>(new ResourceMetrics());
|
||||
|
||||
@BeforeEach
|
||||
@@ -118,18 +117,24 @@ class ResourceMonitorTest {
|
||||
shouldQueue,
|
||||
result,
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"For weight %d and status %s, shouldQueue should be %s",
|
||||
weight, status, shouldQueue));
|
||||
weight,
|
||||
status,
|
||||
shouldQueue));
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourceMetricsShouldDetectStaleState() {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Given
|
||||
Instant now = Instant.now();
|
||||
Instant pastInstant = now.minusMillis(6000);
|
||||
Instant pastInstant =
|
||||
testTime.minusMillis(6000); // 6 seconds ago (relative to test start time)
|
||||
|
||||
ResourceMetrics staleMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, pastInstant);
|
||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, now);
|
||||
ResourceMetrics freshMetrics = new ResourceMetrics(0.5, 0.5, 1024, 2048, 4096, testTime);
|
||||
|
||||
// When/Then
|
||||
assertTrue(
|
||||
|
||||
@@ -5,7 +5,6 @@ import static org.mockito.Mockito.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -42,7 +41,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testCreateTask() {
|
||||
// Act
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-1";
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
// Assert
|
||||
@@ -56,7 +55,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testSetResult() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-2";
|
||||
taskManager.createTask(jobId);
|
||||
Object resultObject = "Test result";
|
||||
|
||||
@@ -74,7 +73,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testSetFileResult() throws Exception {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-3";
|
||||
taskManager.createTask(jobId);
|
||||
String fileId = "file-id";
|
||||
String originalFileName = "test.pdf";
|
||||
@@ -108,7 +107,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testSetError() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-4";
|
||||
taskManager.createTask(jobId);
|
||||
String errorMessage = "Test error";
|
||||
|
||||
@@ -126,7 +125,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testSetComplete_WithExistingResult() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-5";
|
||||
taskManager.createTask(jobId);
|
||||
Object resultObject = "Test result";
|
||||
taskManager.setResult(jobId, resultObject);
|
||||
@@ -144,7 +143,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testSetComplete_WithoutExistingResult() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-6";
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
// Act
|
||||
@@ -160,7 +159,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testIsComplete() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-7";
|
||||
taskManager.createTask(jobId);
|
||||
|
||||
// Assert - not complete initially
|
||||
@@ -215,7 +214,9 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCleanupOldJobs() throws Exception {
|
||||
void testCleanupOldJobs() {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final LocalDateTime testTime = LocalDateTime.now();
|
||||
// Arrange
|
||||
// 1. Create a recent completed job
|
||||
String recentJobId = "recent-job";
|
||||
@@ -227,8 +228,9 @@ class TaskManagerTest {
|
||||
taskManager.createTask(oldJobId);
|
||||
JobResult oldJob = taskManager.getJobResult(oldJobId);
|
||||
|
||||
// Manually set the completion time to be older than the expiry
|
||||
LocalDateTime oldTime = LocalDateTime.now().minusHours(1);
|
||||
// Manually set the completion time to be older than the expiry (relative to test start
|
||||
// time)
|
||||
LocalDateTime oldTime = testTime.minusHours(1);
|
||||
ReflectionTestUtils.setField(oldJob, "completedAt", oldTime);
|
||||
ReflectionTestUtils.setField(oldJob, "complete", true);
|
||||
|
||||
@@ -253,6 +255,7 @@ class TaskManagerTest {
|
||||
taskManager.createTask(activeJobId);
|
||||
|
||||
// Verify all jobs are in the map
|
||||
assertNotNull(jobResultsMap);
|
||||
assertTrue(jobResultsMap.containsKey(recentJobId));
|
||||
assertTrue(jobResultsMap.containsKey(oldJobId));
|
||||
assertTrue(jobResultsMap.containsKey(activeJobId));
|
||||
@@ -268,7 +271,7 @@ class TaskManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testShutdown() throws Exception {
|
||||
void testShutdown() {
|
||||
// This mainly tests that the shutdown method doesn't throw exceptions
|
||||
taskManager.shutdown();
|
||||
|
||||
@@ -279,7 +282,7 @@ class TaskManagerTest {
|
||||
@Test
|
||||
void testAddNote() {
|
||||
// Arrange
|
||||
String jobId = UUID.randomUUID().toString();
|
||||
String jobId = "test-job-8";
|
||||
taskManager.createTask(jobId);
|
||||
String note = "Test note";
|
||||
|
||||
|
||||
+49
-41
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.File;
|
||||
@@ -133,6 +131,9 @@ public class TempFileCleanupServiceTest {
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final long testTime = System.currentTimeMillis();
|
||||
|
||||
// Mock Files.list for each directory we'll process
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(systemTempDir)))
|
||||
@@ -177,18 +178,17 @@ public class TempFileCleanupServiceTest {
|
||||
// maxAgeMillis
|
||||
if (fileName.contains("old")) {
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 5000000);
|
||||
testTime - 5000000); // ~1.4 hours ago
|
||||
}
|
||||
// For empty.tmp file, return a timestamp older than 5 minutes (for
|
||||
// empty file test)
|
||||
else if (fileName.equals("empty.tmp")) {
|
||||
else if ("empty.tmp".equals(fileName)) {
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
||||
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||
}
|
||||
// For all other files, return a recent timestamp
|
||||
else {
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 60000); // 1 minute ago
|
||||
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||
}
|
||||
});
|
||||
|
||||
@@ -201,7 +201,7 @@ public class TempFileCleanupServiceTest {
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
// Return 0 bytes for the empty file
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
if ("empty.tmp".equals(fileName)) {
|
||||
return 0L;
|
||||
}
|
||||
// Return normal size for all other files
|
||||
@@ -221,9 +221,9 @@ public class TempFileCleanupServiceTest {
|
||||
});
|
||||
|
||||
// Act - set containerMode to false for this test
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(customTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||
invokeCleanupDirectoryStreaming(customTempDir, false, 3600000);
|
||||
invokeCleanupDirectoryStreaming(libreOfficeTempDir, false, 3600000);
|
||||
|
||||
// Assert - Only old temp files and empty files should be deleted
|
||||
assertTrue(deletedFiles.contains(oldTempFile), "Old temp file should be deleted");
|
||||
@@ -276,6 +276,9 @@ public class TempFileCleanupServiceTest {
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final long testTime = System.currentTimeMillis();
|
||||
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(systemTempDir)))
|
||||
@@ -290,9 +293,7 @@ public class TempFileCleanupServiceTest {
|
||||
// Configure Files.getLastModifiedTime to return recent timestamps
|
||||
mockedFiles
|
||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
.thenReturn(
|
||||
FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 60000)); // 1 minute ago
|
||||
.thenReturn(FileTime.fromMillis(testTime - 60000)); // 1 minute ago
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L); // 1 KB
|
||||
@@ -308,7 +309,7 @@ public class TempFileCleanupServiceTest {
|
||||
});
|
||||
|
||||
// Act - set containerMode to true and maxAgeMillis to 0 for container startup cleanup
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0, 0);
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, true, 0);
|
||||
|
||||
// Assert - In container mode, both our temp files and system temp files should be
|
||||
// deleted
|
||||
@@ -337,6 +338,9 @@ public class TempFileCleanupServiceTest {
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final long testTime = System.currentTimeMillis();
|
||||
|
||||
// Mock Files.list for systemTempDir
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(systemTempDir)))
|
||||
@@ -356,14 +360,14 @@ public class TempFileCleanupServiceTest {
|
||||
Path path = invocation.getArgument(0);
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
if (fileName.equals("empty.tmp")) {
|
||||
if ("empty.tmp".equals(fileName)) {
|
||||
// More than 5 minutes old
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 6 * 60 * 1000);
|
||||
testTime - 6 * 60 * 1000); // 6 minutes ago
|
||||
} else {
|
||||
// Less than 5 minutes old
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 2 * 60 * 1000);
|
||||
testTime - 2 * 60 * 1000); // 2 minutes ago
|
||||
}
|
||||
});
|
||||
|
||||
@@ -381,7 +385,7 @@ public class TempFileCleanupServiceTest {
|
||||
});
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||
|
||||
// Assert
|
||||
assertTrue(
|
||||
@@ -412,14 +416,25 @@ public class TempFileCleanupServiceTest {
|
||||
|
||||
// Use MockedStatic to mock Files operations
|
||||
try (MockedStatic<Files> mockedFiles = mockStatic(Files.class)) {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final long testTime = System.currentTimeMillis();
|
||||
|
||||
// Mock Files.list for each directory
|
||||
mockedFiles.when(() -> Files.list(eq(systemTempDir))).thenReturn(Stream.of(dir1));
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(systemTempDir)))
|
||||
.thenAnswer(invocation -> Stream.of(dir1));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir1))).thenReturn(Stream.of(tempFile1, dir2));
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(dir1)))
|
||||
.thenAnswer(invocation -> Stream.of(tempFile1, dir2));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir2))).thenReturn(Stream.of(tempFile2, dir3));
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(dir2)))
|
||||
.thenAnswer(invocation -> Stream.of(tempFile2, dir3));
|
||||
|
||||
mockedFiles.when(() -> Files.list(eq(dir3))).thenReturn(Stream.of(tempFile3));
|
||||
mockedFiles
|
||||
.when(() -> Files.list(eq(dir3)))
|
||||
.thenAnswer(invocation -> Stream.of(tempFile3));
|
||||
|
||||
// Configure Files.isDirectory for each path
|
||||
mockedFiles.when(() -> Files.isDirectory(eq(dir1))).thenReturn(true);
|
||||
@@ -432,6 +447,9 @@ public class TempFileCleanupServiceTest {
|
||||
// Configure Files.exists to return true for all paths
|
||||
mockedFiles.when(() -> Files.exists(any(Path.class))).thenReturn(true);
|
||||
|
||||
// Configure Files.size to return 0 for all files (ensure they're not empty)
|
||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
||||
|
||||
// Configure Files.getLastModifiedTime to return different times based on file names
|
||||
mockedFiles
|
||||
.when(() -> Files.getLastModifiedTime(any(Path.class)))
|
||||
@@ -441,19 +459,14 @@ public class TempFileCleanupServiceTest {
|
||||
String fileName = path.getFileName().toString();
|
||||
|
||||
if (fileName.contains("old")) {
|
||||
// Old file
|
||||
return FileTime.fromMillis(
|
||||
System.currentTimeMillis() - 5000000);
|
||||
// Old file - very old timestamp (older than 1 hour)
|
||||
return FileTime.fromMillis(testTime - 7200000); // 2 hours ago
|
||||
} else {
|
||||
// Recent file
|
||||
return FileTime.fromMillis(System.currentTimeMillis() - 60000);
|
||||
// Recent file - very recent timestamp (less than 1 hour)
|
||||
return FileTime.fromMillis(testTime - 60000); // 1 minute ago
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Files.size to return normal size
|
||||
mockedFiles.when(() -> Files.size(any(Path.class))).thenReturn(1024L);
|
||||
|
||||
// For deleteIfExists, track which files would be deleted
|
||||
mockedFiles
|
||||
.when(() -> Files.deleteIfExists(any(Path.class)))
|
||||
.thenAnswer(
|
||||
@@ -463,12 +476,8 @@ public class TempFileCleanupServiceTest {
|
||||
return true;
|
||||
});
|
||||
|
||||
// Act
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 0, 3600000);
|
||||
|
||||
// Debug - print what was deleted
|
||||
System.out.println("Deleted files: " + deletedFiles);
|
||||
System.out.println("Looking for: " + tempFile3);
|
||||
// Act - pass maxAgeMillis = 3600000 (1 hour)
|
||||
invokeCleanupDirectoryStreaming(systemTempDir, false, 3600000);
|
||||
|
||||
// Assert
|
||||
assertFalse(deletedFiles.contains(tempFile1), "Recent temp file should be preserved");
|
||||
@@ -481,8 +490,7 @@ public class TempFileCleanupServiceTest {
|
||||
|
||||
/** Helper method to invoke the private cleanupDirectoryStreaming method using reflection */
|
||||
private void invokeCleanupDirectoryStreaming(
|
||||
Path directory, boolean containerMode, int depth, long maxAgeMillis)
|
||||
throws IOException {
|
||||
Path directory, boolean containerMode, long maxAgeMillis) {
|
||||
try {
|
||||
// Create a consumer that tracks deleted files
|
||||
AtomicInteger deleteCount = new AtomicInteger(0);
|
||||
@@ -505,7 +513,7 @@ public class TempFileCleanupServiceTest {
|
||||
cleanupService,
|
||||
directory,
|
||||
containerMode,
|
||||
depth,
|
||||
0,
|
||||
maxAgeMillis,
|
||||
false,
|
||||
deleteCallback);
|
||||
|
||||
+58
-7
@@ -1,9 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -26,6 +23,7 @@ class CheckProgramInstallTest {
|
||||
|
||||
private MockedStatic<ProcessExecutor> mockProcessExecutor;
|
||||
private ProcessExecutor mockExecutor;
|
||||
private ProcessExecutor mockFfmpegExecutor;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
@@ -34,10 +32,14 @@ class CheckProgramInstallTest {
|
||||
|
||||
// Set up mock for ProcessExecutor
|
||||
mockExecutor = Mockito.mock(ProcessExecutor.class);
|
||||
mockFfmpegExecutor = Mockito.mock(ProcessExecutor.class);
|
||||
mockProcessExecutor = mockStatic(ProcessExecutor.class);
|
||||
mockProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV))
|
||||
.thenReturn(mockExecutor);
|
||||
mockProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.FFMPEG))
|
||||
.thenReturn(mockFfmpegExecutor);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -49,7 +51,7 @@ class CheckProgramInstallTest {
|
||||
}
|
||||
|
||||
/** Reset static fields in the CheckProgramInstall class using reflection */
|
||||
private void resetStaticFields() throws Exception {
|
||||
private static void resetStaticFields() throws Exception {
|
||||
Field pythonAvailableCheckedField =
|
||||
CheckProgramInstall.class.getDeclaredField("pythonAvailableChecked");
|
||||
pythonAvailableCheckedField.setAccessible(true);
|
||||
@@ -59,6 +61,15 @@ class CheckProgramInstallTest {
|
||||
CheckProgramInstall.class.getDeclaredField("availablePythonCommand");
|
||||
availablePythonCommandField.setAccessible(true);
|
||||
availablePythonCommandField.set(null, null);
|
||||
|
||||
Field ffmpegAvailableCheckedField =
|
||||
CheckProgramInstall.class.getDeclaredField("ffmpegAvailableChecked");
|
||||
ffmpegAvailableCheckedField.setAccessible(true);
|
||||
ffmpegAvailableCheckedField.set(null, false);
|
||||
|
||||
Field ffmpegAvailableField = CheckProgramInstall.class.getDeclaredField("ffmpegAvailable");
|
||||
ffmpegAvailableField.setAccessible(true);
|
||||
ffmpegAvailableField.set(null, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -108,8 +119,7 @@ class CheckProgramInstallTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode()
|
||||
throws IOException, InterruptedException, Exception {
|
||||
void testGetAvailablePythonCommand_WhenPythonReturnsNonZeroExitCode() throws Exception {
|
||||
// Arrange
|
||||
// Reset the static fields again to ensure clean state
|
||||
resetStaticFields();
|
||||
@@ -205,4 +215,45 @@ class CheckProgramInstallTest {
|
||||
// Verify getAvailablePythonCommand was called internally
|
||||
verify(mockExecutor).runCommandWithOutputHandling(Arrays.asList("python3", "--version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFfmpegAvailable_WhenInstalled() throws Exception {
|
||||
resetStaticFields();
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||
.thenReturn(result);
|
||||
|
||||
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||
verify(mockFfmpegExecutor)
|
||||
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFfmpegAvailable_WhenNotInstalled() throws Exception {
|
||||
resetStaticFields();
|
||||
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||
.thenThrow(new IOException("Command not found"));
|
||||
|
||||
assertFalse(CheckProgramInstall.isFfmpegAvailable());
|
||||
verify(mockFfmpegExecutor)
|
||||
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsFfmpegAvailable_CachesResult() throws Exception {
|
||||
resetStaticFields();
|
||||
ProcessExecutorResult result = Mockito.mock(ProcessExecutorResult.class);
|
||||
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||
.thenReturn(result);
|
||||
|
||||
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||
|
||||
when(mockFfmpegExecutor.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version")))
|
||||
.thenThrow(new IOException("Command not found"));
|
||||
|
||||
assertTrue(CheckProgramInstall.isFfmpegAvailable());
|
||||
|
||||
verify(mockFfmpegExecutor, times(1))
|
||||
.runCommandWithOutputHandling(Arrays.asList("ffmpeg", "-version"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class ChecksumUtilsTest {
|
||||
@Test
|
||||
void crc32_unsignedFormatting_highBitSet() throws Exception {
|
||||
// CRC32 of single zero byte (0x00) is 0xD202EF8D (>= 0x8000_0000)
|
||||
byte[] data = new byte[] {0x00};
|
||||
byte[] data = {0x00};
|
||||
|
||||
// Hex (unsigned, 8 chars, lowercase)
|
||||
try (InputStream is = new ByteArrayInputStream(data)) {
|
||||
|
||||
+163
-38
@@ -1,41 +1,44 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.SsrfProtectionService;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CustomHtmlSanitizerTest {
|
||||
|
||||
@Mock private SsrfProtectionService ssrfProtectionService;
|
||||
@Mock private ApplicationProperties applicationProperties;
|
||||
@Mock private ApplicationProperties.System systemProperties;
|
||||
|
||||
private CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
SsrfProtectionService mockSsrfProtectionService = mock(SsrfProtectionService.class);
|
||||
stirling.software.common.model.ApplicationProperties mockApplicationProperties =
|
||||
mock(stirling.software.common.model.ApplicationProperties.class);
|
||||
stirling.software.common.model.ApplicationProperties.System mockSystem =
|
||||
mock(stirling.software.common.model.ApplicationProperties.System.class);
|
||||
// Default behavior: allow all URLs and enable sanitization. Lenient stubs avoid
|
||||
// strict-stubbing failures when individual tests bypass certain branches.
|
||||
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(true);
|
||||
lenient().when(applicationProperties.getSystem()).thenReturn(systemProperties);
|
||||
lenient().when(systemProperties.isDisableSanitize()).thenReturn(false);
|
||||
|
||||
// Allow all URLs by default for basic tests
|
||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(true);
|
||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||
when(mockSystem.getDisableSanitize()).thenReturn(false); // Enable sanitization for tests
|
||||
|
||||
customHtmlSanitizer =
|
||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||
customHtmlSanitizer = new CustomHtmlSanitizer(ssrfProtectionService, applicationProperties);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@@ -56,10 +59,11 @@ class CustomHtmlSanitizerTest {
|
||||
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
||||
new String[] {"<p>", "<strong>", "<em>"}),
|
||||
Arguments.of(
|
||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
||||
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
||||
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
||||
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>,"
|
||||
+ " <em>emphasis</em>, <strong>strong</strong>,"
|
||||
+ " <strike>strikethrough</strike>, <s>strike</s>,"
|
||||
+ " <sub>subscript</sub>, <sup>superscript</sup>, <tt>teletype</tt>,"
|
||||
+ " <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||
new String[] {
|
||||
"<b>bold</b>",
|
||||
"<i>italic</i>",
|
||||
@@ -163,7 +167,7 @@ class CustomHtmlSanitizerTest {
|
||||
|
||||
@Test
|
||||
void testSanitizeAllowsImages() {
|
||||
// Arrange - Testing Sanitizers.IMAGES
|
||||
// Arrange - Testing custom images policy with SSRF check
|
||||
String htmlWithImage =
|
||||
"<img src=\"image.jpg\" alt=\"An image\" width=\"100\" height=\"100\">";
|
||||
|
||||
@@ -182,7 +186,16 @@ class CustomHtmlSanitizerTest {
|
||||
void testSanitizeDisallowsDataUrlImages() {
|
||||
// Arrange
|
||||
String htmlWithDataUrlImage =
|
||||
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\" alt=\"SVG with XSS\">";
|
||||
"<img src=\"data:image/svg+xml;base64,PHN2ZyBvbmxvYWQ9ImFsZXJ0KDEpIj48L3N2Zz4=\""
|
||||
+ " alt=\"SVG with XSS\">";
|
||||
|
||||
// Changed: Explicitly tell SSRF service to reject data: URLs so the custom AttributePolicy
|
||||
// drops the src attribute. Without this, a permissive SSRF mock might allow data: URLs.
|
||||
lenient()
|
||||
.when(
|
||||
ssrfProtectionService.isUrlAllowed(
|
||||
argThat(v -> v != null && v.startsWith("data:"))))
|
||||
.thenReturn(false);
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithDataUrlImage);
|
||||
@@ -257,9 +270,9 @@ class CustomHtmlSanitizerTest {
|
||||
void testSanitizeRemovesObjectAndEmbed() {
|
||||
// Arrange
|
||||
String htmlWithObjects =
|
||||
"<p>Safe content</p>"
|
||||
+ "<object data=\"data.swf\" type=\"application/x-shockwave-flash\"></object>"
|
||||
+ "<embed src=\"embed.swf\" type=\"application/x-shockwave-flash\">";
|
||||
"<p>Safe content</p><object data=\"data.swf\""
|
||||
+ " type=\"application/x-shockwave-flash\"></object><embed src=\"embed.swf\""
|
||||
+ " type=\"application/x-shockwave-flash\">";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = customHtmlSanitizer.sanitize(htmlWithObjects);
|
||||
@@ -295,17 +308,12 @@ class CustomHtmlSanitizerTest {
|
||||
void testSanitizeHandlesComplexHtml() {
|
||||
// Arrange
|
||||
String complexHtml =
|
||||
"<div class=\"container\">"
|
||||
+ " <h1 style=\"color: blue;\">Welcome</h1>"
|
||||
+ " <p>This is a <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p>"
|
||||
+ " <table>"
|
||||
+ " <tr><th>Name</th><th>Value</th></tr>"
|
||||
+ " <tr><td>Item 1</td><td>100</td></tr>"
|
||||
+ " </table>"
|
||||
+ " <img src=\"image.jpg\" alt=\"Test image\">"
|
||||
+ " <script>alert('XSS');</script>"
|
||||
+ " <iframe src=\"https://evil.com\"></iframe>"
|
||||
+ "</div>";
|
||||
"<div class=\"container\"> <h1 style=\"color: blue;\">Welcome</h1> <p>This is a"
|
||||
+ " <strong>test</strong> with <a href=\"https://example.com\">link</a>.</p> "
|
||||
+ " <table> <tr><th>Name</th><th>Value</th></tr> <tr><td>Item"
|
||||
+ " 1</td><td>100</td></tr> </table> <img src=\"image.jpg\" alt=\"Test"
|
||||
+ " image\"> <script>alert('XSS');</script> <iframe"
|
||||
+ " src=\"https://evil.com\"></iframe></div>";
|
||||
|
||||
// Act
|
||||
String sanitizedHtml = customHtmlSanitizer.sanitize(complexHtml);
|
||||
@@ -353,4 +361,121 @@ class CustomHtmlSanitizerTest {
|
||||
// Assert
|
||||
assertEquals("", sanitizedHtml, "Null input should result in empty string");
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// Additional coverage
|
||||
// -----------------------
|
||||
|
||||
@Test
|
||||
@DisplayName("Should return input unchanged when sanitize is disabled via properties")
|
||||
void shouldBypassSanitizationWhenDisabled() {
|
||||
// Arrange
|
||||
String malicious =
|
||||
"<p>ok</p><script>alert('XSS')</script><img src=\"http://blocked.local/a.png\">";
|
||||
|
||||
// For this test, disable sanitize
|
||||
when(systemProperties.isDisableSanitize()).thenReturn(true);
|
||||
|
||||
// Also ensure SSRF would block it if sanitization were enabled (to prove bypass)
|
||||
lenient().when(ssrfProtectionService.isUrlAllowed(anyString())).thenReturn(false);
|
||||
|
||||
// Act
|
||||
String result = customHtmlSanitizer.sanitize(malicious);
|
||||
|
||||
// Assert
|
||||
// When disabled, sanitizer must return original string as-is.
|
||||
assertEquals(malicious, result, "Sanitization disabled should return input as-is");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should remove img src when SSRF service rejects the URL")
|
||||
void shouldRemoveImageSrcWhenSsrfRejects() {
|
||||
// Arrange
|
||||
String html = "<img src=\"http://internal/admin\" alt=\"x\">";
|
||||
|
||||
// Reject this URL
|
||||
lenient()
|
||||
.when(ssrfProtectionService.isUrlAllowed("http://internal/admin"))
|
||||
.thenReturn(false);
|
||||
|
||||
// Act
|
||||
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||
assertFalse(
|
||||
sanitized.contains("src=\"http://internal/admin\""),
|
||||
"Rejected URL should be removed from src attribute");
|
||||
assertTrue(sanitized.contains("alt=\"x\""), "Other allowed attributes should remain");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should trim and preserve allowed img src values")
|
||||
void shouldTrimAndPreserveAllowedImgSrc() {
|
||||
// Arrange
|
||||
String html = "<img src=\" https://example.com/image.jpg \" alt=\"pic\" title=\"t\">";
|
||||
|
||||
// Explicit allow (clarity)
|
||||
lenient()
|
||||
.when(ssrfProtectionService.isUrlAllowed("https://example.com/image.jpg"))
|
||||
.thenReturn(true);
|
||||
|
||||
// Act
|
||||
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||
assertFalse(
|
||||
sanitized.contains(" https://example.com/image.jpg "),
|
||||
"Untrimmed src value should not appear in output");
|
||||
assertTrue(
|
||||
sanitized.contains("alt=\"pic\"") || sanitized.contains("alt='pic'"),
|
||||
"Alt attribute should remain");
|
||||
assertTrue(
|
||||
sanitized.contains("title=\"t\"") || sanitized.contains("title='t'"),
|
||||
"Title attribute should remain");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should drop empty or whitespace-only img src values")
|
||||
void shouldDropEmptyImgSrc() {
|
||||
// Arrange
|
||||
String html = "<img src=\" \" alt=\"no src\">";
|
||||
|
||||
// Act
|
||||
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||
|
||||
// Assert
|
||||
assertTrue(sanitized.contains("<img"), "Image element should remain");
|
||||
assertFalse(sanitized.contains("src="), "Empty src should be removed entirely");
|
||||
assertTrue(sanitized.contains("alt=\"no src\""), "Other attributes should remain");
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Anchor tag protocol handling (sanitizer-level)")
|
||||
class AnchorProtocolTests {
|
||||
@Test
|
||||
@DisplayName(
|
||||
"mailto: links should keep anchor and text; protocol may be stripped depending on"
|
||||
+ " sanitizer version")
|
||||
void mailtoAllowed() {
|
||||
String html = "<a href=\"mailto:hello@example.com\">mail me</a>";
|
||||
String sanitized = customHtmlSanitizer.sanitize(html);
|
||||
|
||||
// Anchor and text should remain
|
||||
assertTrue(sanitized.contains("<a"), "Anchor element should be preserved");
|
||||
assertTrue(sanitized.contains("mail me"), "Link text should remain");
|
||||
|
||||
// Some sanitizer versions allow mailto:, others strip it. Accept both.
|
||||
boolean keepsMailto = sanitized.contains("mailto:hello@example.com");
|
||||
boolean strippedHref =
|
||||
sanitized.contains("href="); // still has an href, possibly sanitized
|
||||
// We accept either: mailto kept OR href present in sanitized form OR (in strict setups)
|
||||
// href removed but anchor kept.
|
||||
assertTrue(
|
||||
keepsMailto || strippedHref || sanitized.contains("</a>"),
|
||||
"Sanitized output should keep anchor; mailto: may or may not be present"
|
||||
+ " depending on sanitizer configuration");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -17,6 +12,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
@@ -52,7 +48,7 @@ class EmlToPdfTest {
|
||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(true);
|
||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
||||
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||
|
||||
customHtmlSanitizer =
|
||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||
@@ -765,7 +761,7 @@ class EmlToPdfTest {
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private String getTimestamp() {
|
||||
private static String getTimestamp() {
|
||||
java.time.ZonedDateTime fixedDateTime =
|
||||
java.time.ZonedDateTime.of(2023, 1, 1, 12, 0, 0, 0, java.time.ZoneId.of("GMT"));
|
||||
return java.time.format.DateTimeFormatter.RFC_1123_DATE_TIME.format(fixedDateTime);
|
||||
@@ -778,20 +774,33 @@ class EmlToPdfTest {
|
||||
private String createSimpleTextEmailWithCharset(
|
||||
String from, String to, String subject, String body, String charset) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/plain; charset=%s\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
from, to, subject, getTimestamp(), charset, body);
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
getTimestamp(),
|
||||
charset,
|
||||
body);
|
||||
}
|
||||
|
||||
private String createEmailWithCustomHeaders() {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"From: sender@example.com\nDate: %s\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
getTimestamp(), "This is an email body with some headers missing.");
|
||||
getTimestamp(),
|
||||
"This is an email body with some headers missing.");
|
||||
}
|
||||
|
||||
private String createHtmlEmail(String from, String to, String subject, String htmlBody) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nContent-Type: text/html; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n\n%s",
|
||||
from, to, subject, getTimestamp(), htmlBody);
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
getTimestamp(),
|
||||
htmlBody);
|
||||
}
|
||||
|
||||
private String createMultipartEmailWithAttachment(
|
||||
@@ -806,6 +815,7 @@ class EmlToPdfTest {
|
||||
Base64.getEncoder()
|
||||
.encodeToString(attachmentContent.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
@@ -845,6 +855,7 @@ class EmlToPdfTest {
|
||||
Base64.getEncoder()
|
||||
.encodeToString(attachmentEmlContent.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
@@ -883,6 +894,7 @@ class EmlToPdfTest {
|
||||
private String createMultipartAlternativeEmail(
|
||||
String textBody, String htmlBody, String boundary) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
@@ -918,6 +930,7 @@ class EmlToPdfTest {
|
||||
|
||||
private String createQuotedPrintableEmail() {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: quoted-printable\n\n%s",
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
@@ -930,6 +943,7 @@ class EmlToPdfTest {
|
||||
String encodedBody =
|
||||
Base64.getEncoder().encodeToString(body.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"From: %s\nTo: %s\nSubject: %s\nDate: %s\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: base64\n\n%s",
|
||||
"sender@example.com",
|
||||
"recipient@example.com",
|
||||
@@ -941,6 +955,7 @@ class EmlToPdfTest {
|
||||
private String createEmailWithInlineImage(
|
||||
String htmlBody, String boundary, String contentId, String base64Image) {
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
@@ -985,6 +1000,7 @@ class EmlToPdfTest {
|
||||
String encodedAttachment =
|
||||
Base64.getEncoder().encodeToString(attachmentBody.getBytes(StandardCharsets.UTF_8));
|
||||
return String.format(
|
||||
Locale.ROOT,
|
||||
"""
|
||||
From: %s
|
||||
To: %s
|
||||
@@ -1039,13 +1055,13 @@ class EmlToPdfTest {
|
||||
}
|
||||
|
||||
// Creates a basic EmlToPdfRequest with default settings
|
||||
private EmlToPdfRequest createBasicRequest() {
|
||||
private static EmlToPdfRequest createBasicRequest() {
|
||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||
request.setIncludeAttachments(false);
|
||||
return request;
|
||||
}
|
||||
|
||||
private EmlToPdfRequest createRequestWithAttachments() {
|
||||
private static EmlToPdfRequest createRequestWithAttachments() {
|
||||
EmlToPdfRequest request = new EmlToPdfRequest();
|
||||
request.setIncludeAttachments(true);
|
||||
request.setMaxAttachmentSizeMB(10);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ui.Model;
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ExceptionUtils}. Assumptions: - PdfErrorUtils.isCorruptedPdfError is a
|
||||
* static method that returns true for certain exception types. We will mock it in tests for
|
||||
* handlePdfException and logException.
|
||||
*/
|
||||
class ExceptionUtilsTest {
|
||||
|
||||
@Nested
|
||||
@DisplayName("PDF corruption exception creation")
|
||||
class PdfCorruptionTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("should create PdfCorruptedException without context")
|
||||
void testCreatePdfCorruptedExceptionWithoutContext() {
|
||||
Exception cause = new Exception("root");
|
||||
IOException ex = ExceptionUtils.createPdfCorruptedException(null, cause);
|
||||
|
||||
assertTrue(ex.getMessage().contains("PDF file appears to be corrupted"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create PdfCorruptedException with context")
|
||||
void testCreatePdfCorruptedExceptionWithContext() {
|
||||
Exception cause = new Exception("root");
|
||||
IOException ex = ExceptionUtils.createPdfCorruptedException("during merge", cause);
|
||||
|
||||
assertTrue(
|
||||
ex.getMessage()
|
||||
.startsWith("Error during merge: PDF file appears to be corrupted"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("should create MultiplePdfCorruptedException")
|
||||
void testCreateMultiplePdfCorruptedException() {
|
||||
Exception cause = new Exception("root");
|
||||
IOException ex = ExceptionUtils.createMultiplePdfCorruptedException(cause);
|
||||
|
||||
assertTrue(ex.getMessage().startsWith("One or more PDF files appear to be corrupted"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("PDF encryption and password exception creation")
|
||||
class PdfSecurityTests {
|
||||
|
||||
@Test
|
||||
void testCreatePdfEncryptionException() {
|
||||
Exception cause = new Exception("root");
|
||||
IOException ex = ExceptionUtils.createPdfEncryptionException(cause);
|
||||
assertTrue(ex.getMessage().contains("corrupted encryption data"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePdfPasswordException() {
|
||||
Exception cause = new Exception("root");
|
||||
IOException ex = ExceptionUtils.createPdfPasswordException(cause);
|
||||
assertTrue(ex.getMessage().contains("passworded"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("File processing exception creation")
|
||||
class FileProcessingTests {
|
||||
|
||||
@Test
|
||||
void testCreateFileProcessingException() {
|
||||
Exception cause = new Exception("boom");
|
||||
IOException ex = ExceptionUtils.createFileProcessingException("merge", cause);
|
||||
assertTrue(ex.getMessage().contains("while processing the file during merge"));
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Generic exception creation")
|
||||
class GenericCreationTests {
|
||||
|
||||
@Test
|
||||
void testCreateIOException() {
|
||||
IOException ex =
|
||||
ExceptionUtils.createIOException(
|
||||
"key", "Default message: {0}", new Exception("cause"), "X");
|
||||
assertEquals("Default message: X", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateRuntimeException() {
|
||||
RuntimeException ex =
|
||||
ExceptionUtils.createRuntimeException(
|
||||
"key", "Default message: {0}", new Exception("cause"), "Y");
|
||||
assertEquals("Default message: Y", ex.getMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateIllegalArgumentException() {
|
||||
IllegalArgumentException ex =
|
||||
ExceptionUtils.createIllegalArgumentException("key", "Format {0}", "Z");
|
||||
assertEquals("Format Z", ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Predefined validation exceptions")
|
||||
class PredefinedValidationTests {
|
||||
|
||||
@Test
|
||||
void testCreateHtmlFileRequiredException() {
|
||||
IllegalArgumentException ex = ExceptionUtils.createHtmlFileRequiredException();
|
||||
assertTrue(ex.getMessage().contains("HTML or ZIP"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePdfFileRequiredException() {
|
||||
IllegalArgumentException ex = ExceptionUtils.createPdfFileRequiredException();
|
||||
assertTrue(ex.getMessage().contains("PDF"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateInvalidPageSizeException() {
|
||||
IllegalArgumentException ex = ExceptionUtils.createInvalidPageSizeException("A5");
|
||||
assertTrue(ex.getMessage().contains("page size"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("OCR and system requirement exceptions")
|
||||
class OcrAndSystemTests {
|
||||
|
||||
@Test
|
||||
void testCreateOcrLanguageRequiredException() {
|
||||
IOException ex = ExceptionUtils.createOcrLanguageRequiredException();
|
||||
assertTrue(ex.getMessage().contains("OCR language"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateOcrInvalidLanguagesException() {
|
||||
IOException ex = ExceptionUtils.createOcrInvalidLanguagesException();
|
||||
assertTrue(ex.getMessage().contains("none of the selected languages"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateOcrToolsUnavailableException() {
|
||||
IOException ex = ExceptionUtils.createOcrToolsUnavailableException();
|
||||
assertTrue(ex.getMessage().contains("OCR tools"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreatePythonRequiredForWebpException() {
|
||||
IOException ex = ExceptionUtils.createPythonRequiredForWebpException();
|
||||
assertTrue(ex.getMessage().contains("Python"));
|
||||
assertTrue(ex.getMessage().contains("WebP conversion"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("File operation and compression exceptions")
|
||||
class FileAndCompressionTests {
|
||||
|
||||
@Test
|
||||
void testCreatePdfaConversionFailedException() {
|
||||
RuntimeException ex = ExceptionUtils.createPdfaConversionFailedException();
|
||||
assertTrue(ex.getMessage().contains("PDF/A conversion failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateMd5AlgorithmException() {
|
||||
RuntimeException ex = ExceptionUtils.createMd5AlgorithmException(new Exception("x"));
|
||||
assertTrue(ex.getMessage().contains("MD5"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateGhostscriptCompressionExceptionNoCause() {
|
||||
IOException ex = ExceptionUtils.createGhostscriptCompressionException();
|
||||
assertTrue(ex.getMessage().contains("Ghostscript"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateGhostscriptCompressionExceptionWithCause() {
|
||||
IOException ex =
|
||||
ExceptionUtils.createGhostscriptCompressionException(new Exception("cause"));
|
||||
assertTrue(ex.getMessage().contains("Ghostscript"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("PDF exception handling")
|
||||
class PdfExceptionHandlingTests {
|
||||
|
||||
@Test
|
||||
void testHandlePdfExceptionWhenCorrupted() {
|
||||
IOException original = new IOException("corrupted pdf");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(true);
|
||||
IOException result = ExceptionUtils.handlePdfException(original);
|
||||
assertNotSame(original, result);
|
||||
assertTrue(result.getMessage().contains("corrupted"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandlePdfExceptionWhenEncryptionError() {
|
||||
IOException original = new IOException("BadPaddingException");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||
IOException result = ExceptionUtils.handlePdfException(original);
|
||||
assertTrue(result.getMessage().contains("corrupted encryption data"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandlePdfExceptionWhenPasswordError() {
|
||||
IOException original = new IOException("password is incorrect");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||
IOException result = ExceptionUtils.handlePdfException(original);
|
||||
assertTrue(result.getMessage().contains("passworded"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHandlePdfExceptionWhenNoSpecialError() {
|
||||
IOException original = new IOException("something else");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(original)).thenReturn(false);
|
||||
IOException result = ExceptionUtils.handlePdfException(original);
|
||||
assertSame(original, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Encryption and password detection")
|
||||
class ErrorDetectionTests {
|
||||
|
||||
@Test
|
||||
void testIsEncryptionErrorTrue() {
|
||||
assertTrue(ExceptionUtils.isEncryptionError(new IOException("BadPaddingException")));
|
||||
assertTrue(
|
||||
ExceptionUtils.isEncryptionError(
|
||||
new IOException("Given final block not properly padded")));
|
||||
assertTrue(
|
||||
ExceptionUtils.isEncryptionError(
|
||||
new IOException("AES initialization vector not fully read")));
|
||||
assertTrue(ExceptionUtils.isEncryptionError(new IOException("Failed to decrypt")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsEncryptionErrorFalse() {
|
||||
assertFalse(ExceptionUtils.isEncryptionError(new IOException("other message")));
|
||||
assertFalse(ExceptionUtils.isEncryptionError(new IOException((String) null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPasswordErrorTrue() {
|
||||
assertTrue(ExceptionUtils.isPasswordError(new IOException("password is incorrect")));
|
||||
assertTrue(ExceptionUtils.isPasswordError(new IOException("Password is not provided")));
|
||||
assertTrue(
|
||||
ExceptionUtils.isPasswordError(
|
||||
new IOException("PDF contains an encryption dictionary")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsPasswordErrorFalse() {
|
||||
assertFalse(ExceptionUtils.isPasswordError(new IOException("something else")));
|
||||
assertFalse(ExceptionUtils.isPasswordError(new IOException((String) null)));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Logging behavior")
|
||||
class LoggingTests {
|
||||
|
||||
@Test
|
||||
void testLogExceptionWhenCorruptedPdf() {
|
||||
Exception e = new IOException("corrupted");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(true);
|
||||
// We can't assert log output here without a custom appender, but this ensures no
|
||||
// exception is thrown
|
||||
ExceptionUtils.logException("merge", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogExceptionWhenEncryptionError() {
|
||||
IOException e = new IOException("BadPaddingException");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||
ExceptionUtils.logException("merge", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogExceptionWhenPasswordError() {
|
||||
IOException e = new IOException("password is incorrect");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||
ExceptionUtils.logException("merge", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLogExceptionUnexpectedError() {
|
||||
Exception e = new RuntimeException("unexpected");
|
||||
try (MockedStatic<PdfErrorUtils> mock = mockStatic(PdfErrorUtils.class)) {
|
||||
mock.when(() -> PdfErrorUtils.isCorruptedPdfError(e)).thenReturn(false);
|
||||
ExceptionUtils.logException("merge", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Invalid and null argument exceptions")
|
||||
class ArgumentValidationTests {
|
||||
|
||||
@Test
|
||||
void testCreateInvalidArgumentExceptionSingle() {
|
||||
IllegalArgumentException ex =
|
||||
ExceptionUtils.createInvalidArgumentException("arg", "invalidValue");
|
||||
assertTrue(ex.getMessage().contains("arg"));
|
||||
assertTrue(ex.getMessage().contains("invalidValue"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateInvalidArgumentExceptionWithValue() {
|
||||
IllegalArgumentException ex =
|
||||
ExceptionUtils.createInvalidArgumentException("arg", "val");
|
||||
assertTrue(ex.getMessage().contains("val"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateNullArgumentException() {
|
||||
IllegalArgumentException ex = ExceptionUtils.createNullArgumentException("arg");
|
||||
assertTrue(ex.getMessage().contains("arg"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -46,12 +45,15 @@ class FileMonitorTest {
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_OldFile() throws IOException {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("test-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
// Set modified time to 10 seconds ago (relative to test start time)
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||
|
||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||
assertTrue(fileMonitor.isFileReadyForProcessing(testFile));
|
||||
@@ -59,12 +61,15 @@ class FileMonitorTest {
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_RecentFile() throws IOException {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("recent-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to just now
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now()));
|
||||
// Set modified time to just now (relative to test start time)
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime));
|
||||
|
||||
// File should not be ready for processing as it was just modified
|
||||
assertFalse(fileMonitor.isFileReadyForProcessing(testFile));
|
||||
@@ -81,12 +86,16 @@ class FileMonitorTest {
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_LockedFile() throws IOException {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("locked-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago to make sure it passes the time check
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
// Set modified time to 10 seconds ago (relative to test start time) to make sure it passes
|
||||
// the time check
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||
|
||||
// Verify the file is considered ready when it meets the time criteria
|
||||
assertTrue(
|
||||
@@ -105,12 +114,12 @@ class FileMonitorTest {
|
||||
// Create a PDF file
|
||||
Path pdfFile = tempDir.resolve("test.pdf");
|
||||
Files.write(pdfFile, "pdf content".getBytes());
|
||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
Files.setLastModifiedTime(pdfFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||
|
||||
// Create a TXT file
|
||||
Path txtFile = tempDir.resolve("test.txt");
|
||||
Files.write(txtFile, "text content".getBytes());
|
||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
Files.setLastModifiedTime(txtFile, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||
|
||||
// PDF file should be ready for processing
|
||||
assertTrue(pdfMonitor.isFileReadyForProcessing(pdfFile));
|
||||
@@ -126,12 +135,15 @@ class FileMonitorTest {
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_FileInUse() throws IOException {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("in-use-file.txt");
|
||||
Files.write(testFile, "initial content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
// Set modified time to 10 seconds ago (relative to test start time)
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||
|
||||
// First check that the file is ready when meeting time criteria
|
||||
assertTrue(
|
||||
@@ -140,7 +152,7 @@ class FileMonitorTest {
|
||||
|
||||
// After modifying the file to simulate closing, it should still be ready
|
||||
Files.write(testFile, "updated content".getBytes());
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||
|
||||
assertTrue(
|
||||
fileMonitor.isFileReadyForProcessing(testFile),
|
||||
@@ -149,12 +161,15 @@ class FileMonitorTest {
|
||||
|
||||
@Test
|
||||
void testIsFileReadyForProcessing_FileWithAbsolutePath() throws IOException {
|
||||
// Capture test time at the beginning for deterministic calculations
|
||||
final Instant testTime = Instant.now();
|
||||
|
||||
// Create a test file
|
||||
Path testFile = tempDir.resolve("absolute-path-file.txt");
|
||||
Files.write(testFile, "test content".getBytes());
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
// Set modified time to 10 seconds ago (relative to test start time)
|
||||
Files.setLastModifiedTime(testFile, FileTime.from(testTime.minusMillis(10000)));
|
||||
|
||||
// File should be ready for processing as it was modified more than 5 seconds ago
|
||||
// Use the absolute path to make sure it's handled correctly
|
||||
@@ -168,7 +183,7 @@ class FileMonitorTest {
|
||||
Files.createDirectory(testDir);
|
||||
|
||||
// Set modified time to 10 seconds ago
|
||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.now().minusMillis(10000)));
|
||||
Files.setLastModifiedTime(testDir, FileTime.from(Instant.ofEpochMilli(1000000L)));
|
||||
|
||||
// A directory should not be considered ready for processing
|
||||
boolean isReady = fileMonitor.isFileReadyForProcessing(testDir);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -31,7 +29,7 @@ public class FileToPdfTest {
|
||||
when(mockSsrfProtectionService.isUrlAllowed(org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(true);
|
||||
when(mockApplicationProperties.getSystem()).thenReturn(mockSystem);
|
||||
when(mockSystem.getDisableSanitize()).thenReturn(false);
|
||||
when(mockSystem.isDisableSanitize()).thenReturn(false);
|
||||
|
||||
customHtmlSanitizer =
|
||||
new CustomHtmlSanitizer(mockSsrfProtectionService, mockApplicationProperties);
|
||||
|
||||
+15
@@ -29,6 +29,21 @@ class GeneralUtilsAdditionalTest {
|
||||
assertTrue(GeneralUtils.isValidURL("https://example.com"));
|
||||
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://0.0.0.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://192.168.1.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://169.254.0.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://172.16.0.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://192.0.2.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://192.0.0.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://192.168.0.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://198.18.0.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://198.51.100.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://203.0.113.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://10.0.0.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://100.64.0.1"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://224.0.0.0"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://[::ffff:127.0.0.1]/"));
|
||||
assertFalse(GeneralUtils.isURLReachable("http://[fd12:3456:789a::1]/"));
|
||||
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
|
||||
|
||||
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
|
||||
|
||||
+13
-15
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
@@ -11,10 +9,18 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
public class ImageProcessingUtilsTest {
|
||||
|
||||
private static void fillImageWithColor(BufferedImage image) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
image.setRGB(x, y, Color.RED.getRGB());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConvertColorTypeToGreyscale() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
fillImageWithColor(sourceImage);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "greyscale");
|
||||
@@ -33,7 +39,7 @@ public class ImageProcessingUtilsTest {
|
||||
@Test
|
||||
void testConvertColorTypeToBlackWhite() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
fillImageWithColor(sourceImage);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "blackwhite");
|
||||
@@ -51,7 +57,7 @@ public class ImageProcessingUtilsTest {
|
||||
@Test
|
||||
void testConvertColorTypeToFullColor() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
fillImageWithColor(sourceImage);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "fullcolor");
|
||||
@@ -63,7 +69,7 @@ public class ImageProcessingUtilsTest {
|
||||
@Test
|
||||
void testConvertColorTypeInvalid() {
|
||||
BufferedImage sourceImage = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
|
||||
fillImageWithColor(sourceImage, Color.RED);
|
||||
fillImageWithColor(sourceImage);
|
||||
|
||||
BufferedImage convertedImage =
|
||||
ImageProcessingUtils.convertColorType(sourceImage, "invalidtype");
|
||||
@@ -71,12 +77,4 @@ public class ImageProcessingUtilsTest {
|
||||
assertNotNull(convertedImage);
|
||||
assertEquals(sourceImage, convertedImage);
|
||||
}
|
||||
|
||||
private void fillImageWithColor(BufferedImage image, Color color) {
|
||||
for (int y = 0; y < image.getHeight(); y++) {
|
||||
for (int x = 0; x < image.getWidth(); x++) {
|
||||
image.setRGB(x, y, color.getRGB());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
@@ -34,6 +32,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.ZipSecurity;
|
||||
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
|
||||
/**
|
||||
@@ -50,6 +49,7 @@ class PDFToFileTest {
|
||||
@Mock private ProcessExecutor mockProcessExecutor;
|
||||
@Mock private ProcessExecutorResult mockExecutorResult;
|
||||
@Mock private TempFileManager mockTempFileManager;
|
||||
@Mock private RuntimePathConfig mockRuntimePathConfig;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
@@ -63,7 +63,9 @@ class PDFToFileTest {
|
||||
.when(mockTempFileManager.createTempDirectory())
|
||||
.thenAnswer(invocation -> Files.createTempDirectory("test"));
|
||||
|
||||
pdfToFile = new PDFToFile(mockTempFileManager);
|
||||
lenient().when(mockRuntimePathConfig.getSOfficePath()).thenReturn("/usr/bin/soffice");
|
||||
|
||||
pdfToFile = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -329,12 +331,10 @@ class PDFToFileTest {
|
||||
boolean foundImage = false;
|
||||
|
||||
while ((entry = zipStream.getNextEntry()) != null) {
|
||||
if ("test.html".equals(entry.getName())) {
|
||||
foundMainHtml = true;
|
||||
} else if ("test_ind.html".equals(entry.getName())) {
|
||||
foundIndexHtml = true;
|
||||
} else if ("test_img.png".equals(entry.getName())) {
|
||||
foundImage = true;
|
||||
switch (entry.getName()) {
|
||||
case "test.html" -> foundMainHtml = true;
|
||||
case "test_ind.html" -> foundIndexHtml = true;
|
||||
case "test_img.png" -> foundImage = true;
|
||||
}
|
||||
zipStream.closeEntry();
|
||||
}
|
||||
@@ -367,7 +367,8 @@ class PDFToFileTest {
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(
|
||||
args ->
|
||||
args.contains("--convert-to")
|
||||
args != null
|
||||
&& args.contains("--convert-to")
|
||||
&& args.contains("docx"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
@@ -382,6 +383,7 @@ class PDFToFileTest {
|
||||
}
|
||||
|
||||
// Create output file
|
||||
assertNotNull(outDir);
|
||||
Files.write(
|
||||
Path.of(outDir, "document.docx"),
|
||||
"Fake DOCX content".getBytes());
|
||||
@@ -427,7 +429,11 @@ class PDFToFileTest {
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(args -> args.contains("--convert-to") && args.contains("odp"))))
|
||||
argThat(
|
||||
args ->
|
||||
args != null
|
||||
&& args.contains("--convert-to")
|
||||
&& args.contains("odp"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
// When command is executed, find the output directory argument
|
||||
@@ -442,6 +448,7 @@ class PDFToFileTest {
|
||||
|
||||
// Create multiple output files (simulating a presentation with
|
||||
// multiple files)
|
||||
assertNotNull(outDir);
|
||||
Files.write(
|
||||
Path.of(outDir, "document.odp"),
|
||||
"Fake ODP content".getBytes());
|
||||
@@ -515,7 +522,8 @@ class PDFToFileTest {
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(
|
||||
args ->
|
||||
args.contains("--convert-to")
|
||||
args != null
|
||||
&& args.contains("--convert-to")
|
||||
&& args.contains("txt:Text"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
@@ -530,6 +538,7 @@ class PDFToFileTest {
|
||||
}
|
||||
|
||||
// Create text output file
|
||||
assertNotNull(outDir);
|
||||
Files.write(
|
||||
Path.of(outDir, "document.txt"),
|
||||
"Extracted text content".getBytes());
|
||||
@@ -587,6 +596,7 @@ class PDFToFileTest {
|
||||
}
|
||||
|
||||
// Create output file - uses default name
|
||||
assertNotNull(outDir);
|
||||
Files.write(
|
||||
Path.of(outDir, "output.docx"),
|
||||
"Fake DOCX content".getBytes());
|
||||
@@ -611,4 +621,110 @@ class PDFToFileTest {
|
||||
.contains("output.docx"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_UsesUnoconvertWhenConfigured()
|
||||
throws IOException, InterruptedException {
|
||||
when(mockRuntimePathConfig.getUnoConvertPath()).thenReturn("/custom/unoconvert");
|
||||
PDFToFile pdfToFileWithUno = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(args -> args != null && args.contains("/custom/unoconvert"))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outputPath = args.get(args.size() - 1);
|
||||
Files.write(Path.of(outputPath), "Fake DOCX content".getBytes());
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("document.docx"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProcessPdfToOfficeFormat_FallsBackWhenUnoconvertFails()
|
||||
throws IOException, InterruptedException {
|
||||
when(mockRuntimePathConfig.getUnoConvertPath()).thenReturn("/custom/unoconvert");
|
||||
PDFToFile pdfToFileWithUno = new PDFToFile(mockTempFileManager, mockRuntimePathConfig);
|
||||
|
||||
try (MockedStatic<ProcessExecutor> mockedStaticProcessExecutor =
|
||||
mockStatic(ProcessExecutor.class)) {
|
||||
MultipartFile pdfFile =
|
||||
new MockMultipartFile(
|
||||
"file",
|
||||
"document.pdf",
|
||||
MediaType.APPLICATION_PDF_VALUE,
|
||||
"Fake PDF content".getBytes());
|
||||
|
||||
mockedStaticProcessExecutor
|
||||
.when(() -> ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE))
|
||||
.thenReturn(mockProcessExecutor);
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(args -> args != null && args.contains("/custom/unoconvert"))))
|
||||
.thenThrow(new IOException("Conversion failed"));
|
||||
|
||||
when(mockProcessExecutor.runCommandWithOutputHandling(
|
||||
argThat(
|
||||
args ->
|
||||
args != null
|
||||
&& args.stream()
|
||||
.anyMatch(
|
||||
arg ->
|
||||
arg.contains(
|
||||
"soffice")))))
|
||||
.thenAnswer(
|
||||
invocation -> {
|
||||
List<String> args = invocation.getArgument(0);
|
||||
String outDir = null;
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
if ("--outdir".equals(args.get(i)) && i + 1 < args.size()) {
|
||||
outDir = args.get(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
assertNotNull(outDir);
|
||||
Files.write(
|
||||
Path.of(outDir, "document.docx"),
|
||||
"Fallback DOCX content".getBytes());
|
||||
return mockExecutorResult;
|
||||
});
|
||||
|
||||
ResponseEntity<byte[]> response =
|
||||
pdfToFileWithUno.processPdfToOfficeFormat(pdfFile, "docx", "writer_pdf_import");
|
||||
|
||||
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||
assertNotNull(response.getBody());
|
||||
assertTrue(response.getBody().length > 0);
|
||||
assertTrue(
|
||||
response.getHeaders()
|
||||
.getContentDisposition()
|
||||
.toString()
|
||||
.contains("document.docx"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
package stirling.software.common.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.RenderedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
|
||||
import org.apache.pdfbox.pdmodel.PDResources;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -31,32 +49,77 @@ public class PdfUtilsTest {
|
||||
|
||||
@Test
|
||||
void testTextToPageSize() {
|
||||
assertEquals(PDRectangle.A0, PdfUtils.textToPageSize("A0"));
|
||||
assertEquals(PDRectangle.A1, PdfUtils.textToPageSize("A1"));
|
||||
assertEquals(PDRectangle.A2, PdfUtils.textToPageSize("A2"));
|
||||
assertEquals(PDRectangle.A3, PdfUtils.textToPageSize("A3"));
|
||||
assertEquals(PDRectangle.A4, PdfUtils.textToPageSize("A4"));
|
||||
assertEquals(PDRectangle.A5, PdfUtils.textToPageSize("A5"));
|
||||
assertEquals(PDRectangle.A6, PdfUtils.textToPageSize("A6"));
|
||||
assertEquals(PDRectangle.LETTER, PdfUtils.textToPageSize("LETTER"));
|
||||
assertEquals(PDRectangle.LEGAL, PdfUtils.textToPageSize("LEGAL"));
|
||||
assertThrows(IllegalArgumentException.class, () -> PdfUtils.textToPageSize("INVALID"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testHasImagesOnPage() throws IOException {
|
||||
// Mock a PDPage and its resources
|
||||
PDPage page = Mockito.mock(PDPage.class);
|
||||
PDResources resources = Mockito.mock(PDResources.class);
|
||||
Mockito.when(page.getResources()).thenReturn(resources);
|
||||
void testGetAllImages() throws Exception {
|
||||
// Root resources
|
||||
PDResources root = mock(PDResources.class);
|
||||
|
||||
// Case 1: No images in resources
|
||||
Mockito.when(resources.getXObjectNames()).thenReturn(Collections.emptySet());
|
||||
assertFalse(PdfUtils.hasImagesOnPage(page));
|
||||
COSName im1 = COSName.getPDFName("Im1");
|
||||
COSName form1 = COSName.getPDFName("Form1");
|
||||
COSName other1 = COSName.getPDFName("Other1");
|
||||
when(root.getXObjectNames()).thenReturn(Arrays.asList(im1, form1, other1));
|
||||
|
||||
// Case 2: Resources with an image
|
||||
Set<COSName> xObjectNames = new HashSet<>();
|
||||
COSName cosName = Mockito.mock(COSName.class);
|
||||
xObjectNames.add(cosName);
|
||||
// Direct image at root
|
||||
PDImageXObject imgXObj1 = mock(PDImageXObject.class);
|
||||
BufferedImage img1 = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB);
|
||||
when(imgXObj1.getImage()).thenReturn(img1);
|
||||
when(root.getXObject(im1)).thenReturn(imgXObj1);
|
||||
|
||||
PDImageXObject imageXObject = Mockito.mock(PDImageXObject.class);
|
||||
Mockito.when(resources.getXObjectNames()).thenReturn(xObjectNames);
|
||||
Mockito.when(resources.getXObject(cosName)).thenReturn(imageXObject);
|
||||
// "Other" XObject that should be ignored
|
||||
PDXObject otherXObj = mock(PDXObject.class);
|
||||
when(root.getXObject(other1)).thenReturn(otherXObj);
|
||||
|
||||
assertTrue(PdfUtils.hasImagesOnPage(page));
|
||||
// Form XObject with its own resources
|
||||
PDFormXObject formXObj = mock(PDFormXObject.class);
|
||||
PDResources formRes = mock(PDResources.class);
|
||||
when(formXObj.getResources()).thenReturn(formRes);
|
||||
when(root.getXObject(form1)).thenReturn(formXObj);
|
||||
|
||||
// Inside the form: one image and a nested form
|
||||
COSName im2 = COSName.getPDFName("Im2");
|
||||
COSName nestedForm = COSName.getPDFName("NestedForm");
|
||||
when(formRes.getXObjectNames()).thenReturn(Arrays.asList(im2, nestedForm));
|
||||
|
||||
PDImageXObject imgXObj2 = mock(PDImageXObject.class);
|
||||
BufferedImage img2 = new BufferedImage(3, 3, BufferedImage.TYPE_INT_RGB);
|
||||
when(imgXObj2.getImage()).thenReturn(img2);
|
||||
when(formRes.getXObject(im2)).thenReturn(imgXObj2);
|
||||
|
||||
PDFormXObject nestedFormXObj = mock(PDFormXObject.class);
|
||||
PDResources nestedRes = mock(PDResources.class);
|
||||
when(nestedFormXObj.getResources()).thenReturn(nestedRes);
|
||||
when(formRes.getXObject(nestedForm)).thenReturn(nestedFormXObj);
|
||||
|
||||
// Deep nest: another image
|
||||
COSName im3 = COSName.getPDFName("Im3");
|
||||
when(nestedRes.getXObjectNames()).thenReturn(List.of(im3));
|
||||
|
||||
PDImageXObject imgXObj3 = mock(PDImageXObject.class);
|
||||
BufferedImage img3 = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||
when(imgXObj3.getImage()).thenReturn(img3);
|
||||
when(nestedRes.getXObject(im3)).thenReturn(imgXObj3);
|
||||
|
||||
// Act
|
||||
List<RenderedImage> result = PdfUtils.getAllImages(root);
|
||||
|
||||
// Assert
|
||||
assertEquals(
|
||||
3, result.size(), "It should find exactly 3 images (root + form + nested form).");
|
||||
assertTrue(
|
||||
result.containsAll(List.of(img1, img2, img3)),
|
||||
"All expected images must be present.");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,7 +170,7 @@ public class PdfUtilsTest {
|
||||
g.fillRect(0, 0, 10, 10);
|
||||
g.dispose();
|
||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
||||
javax.imageio.ImageIO.write(image, "png", imgOut);
|
||||
ImageIO.write(image, "png", imgOut);
|
||||
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
@@ -120,4 +183,554 @@ public class PdfUtilsTest {
|
||||
assertEquals(1, resultDoc.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================================================
|
||||
// Additional tests (added without modifying existing ones)
|
||||
// ===============================================================
|
||||
|
||||
/* Helper: create a colored test image */
|
||||
private static BufferedImage createImage(int w, int h, Color color) {
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
g.setColor(color);
|
||||
g.fillRect(0, 0, w, h);
|
||||
g.dispose();
|
||||
return img;
|
||||
}
|
||||
|
||||
/* Helper: create a factory like in existing tests */
|
||||
private static CustomPDFDocumentFactory factory() {
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
return new CustomPDFDocumentFactory(meta);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertPdfToPdfImage: creates image-PDF with same page count")
|
||||
void convertPdfToPdfImage_shouldCreateImagePdfWithSamePageCount() throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p1);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("Hello PDF");
|
||||
cs.endText();
|
||||
}
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p2);
|
||||
|
||||
PDDocument out = PdfUtils.convertPdfToPdfImage(doc);
|
||||
assertNotNull(out);
|
||||
assertEquals(2, out.getNumberOfPages(), "Page count should be preserved");
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("imageToPdf: PNG -> single-page PDF (static ImageProcessingUtils mocked)")
|
||||
void imageToPdf_shouldCreatePdfFromPng() throws Exception {
|
||||
BufferedImage img = createImage(320, 200, Color.RED);
|
||||
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "png", pngOut);
|
||||
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile("files", "test.png", "image/png", pngOut.toByteArray());
|
||||
|
||||
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
||||
// Assume: loadImageWithExifOrientation/convertColorType exist – static mock
|
||||
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
||||
.thenReturn(img);
|
||||
mocked.when(
|
||||
() ->
|
||||
ImageProcessingUtils.convertColorType(
|
||||
any(BufferedImage.class), anyString()))
|
||||
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
||||
|
||||
byte[] pdfBytes =
|
||||
PdfUtils.imageToPdf(
|
||||
new MockMultipartFile[] {file},
|
||||
"maintainAspectRatio",
|
||||
true,
|
||||
"RGB",
|
||||
factory());
|
||||
|
||||
try (PDDocument result = factory().load(pdfBytes)) {
|
||||
assertEquals(1, result.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("imageToPdf: JPEG -> single-page PDF (JPEGFactory path)")
|
||||
void imageToPdf_shouldCreatePdfFromJpeg_UsingJpegFactory() throws Exception {
|
||||
BufferedImage img = createImage(640, 360, Color.BLUE);
|
||||
ByteArrayOutputStream jpgOut = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "jpg", jpgOut);
|
||||
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile("files", "photo.jpg", "image/jpeg", jpgOut.toByteArray());
|
||||
|
||||
try (MockedStatic<ImageProcessingUtils> mocked = mockStatic(ImageProcessingUtils.class)) {
|
||||
mocked.when(() -> ImageProcessingUtils.loadImageWithExifOrientation(any()))
|
||||
.thenReturn(img);
|
||||
mocked.when(
|
||||
() ->
|
||||
ImageProcessingUtils.convertColorType(
|
||||
any(BufferedImage.class), anyString()))
|
||||
.thenAnswer(inv -> inv.getArgument(0, BufferedImage.class));
|
||||
|
||||
byte[] pdfBytes =
|
||||
PdfUtils.imageToPdf(
|
||||
new MockMultipartFile[] {file}, "fillPage", false, "RGB", factory());
|
||||
|
||||
try (PDDocument result = factory().load(pdfBytes)) {
|
||||
assertEquals(1, result.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("addImageToDocument: fitDocumentToImage -> page size = image size")
|
||||
void addImageToDocument_shouldUseImageSizeForPage_whenFitDocumentToImage() throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
BufferedImage img = createImage(300, 500, Color.GREEN);
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||
|
||||
PdfUtils.addImageToDocument(doc, ximg, "fitDocumentToImage", false);
|
||||
|
||||
assertEquals(1, doc.getNumberOfPages());
|
||||
PDRectangle box = doc.getPage(0).getMediaBox();
|
||||
assertEquals(300, (int) box.getWidth());
|
||||
assertEquals(500, (int) box.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("addImageToDocument: autoRotate rotates A4 for landscape image")
|
||||
void addImageToDocument_shouldRotateA4_whenAutoRotateAndLandscape() throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
BufferedImage img = createImage(800, 400, Color.ORANGE); // Landscape
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||
|
||||
PdfUtils.addImageToDocument(doc, ximg, "maintainAspectRatio", true);
|
||||
|
||||
assertEquals(1, doc.getNumberOfPages());
|
||||
PDRectangle box = doc.getPage(0).getMediaBox();
|
||||
assertTrue(
|
||||
box.getWidth() > box.getHeight(),
|
||||
"A4 should be landscape when auto-rotate + landscape");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("addImageToDocument: fillPage runs without errors")
|
||||
void addImageToDocument_fillPage_executes() throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
BufferedImage img = createImage(200, 200, Color.MAGENTA);
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, img);
|
||||
|
||||
PdfUtils.addImageToDocument(doc, ximg, "fillPage", false);
|
||||
|
||||
assertEquals(1, doc.getNumberOfPages());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("overlayImage: everyPage=true overlays all pages")
|
||||
void overlayImage_shouldOverlayAllPages_whenEveryPageTrue() throws IOException {
|
||||
CustomPDFDocumentFactory factory = factory();
|
||||
|
||||
// Create PDF with 2 pages
|
||||
byte[] basePdf;
|
||||
try (PDDocument doc = factory.createNewDocument()) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
basePdf = baos.toByteArray();
|
||||
}
|
||||
|
||||
// Create image bytes
|
||||
BufferedImage img = createImage(50, 50, Color.BLACK);
|
||||
ByteArrayOutputStream pngOut = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "png", pngOut);
|
||||
|
||||
byte[] result = PdfUtils.overlayImage(factory, basePdf, pngOut.toByteArray(), 10, 10, true);
|
||||
|
||||
try (PDDocument out = factory.load(result)) {
|
||||
assertEquals(2, out.getNumberOfPages(), "Page count remains identical");
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper function: document with text on page1/page2 */
|
||||
private static PDDocument createDocWithText(String p1, String p2) throws IOException {
|
||||
PDDocument doc = new PDDocument();
|
||||
|
||||
PDPage page1 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(page1);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, page1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText(p1);
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
PDPage page2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(page2);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, page2, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText(p2);
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("containsTextInFile: pagesToCheck='all' finds text")
|
||||
void containsTextInFile_allPages_true() throws IOException {
|
||||
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
||||
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "all"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("containsTextInFile: single page '2' finds text")
|
||||
void containsTextInFile_singlePage_two_true() throws IOException {
|
||||
try (PDDocument doc = createDocWithText("alpha", "beta")) {
|
||||
assertTrue(PdfUtils.containsTextInFile(doc, "beta", "2"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("containsTextInFile: range '1-1' finds text on page 1")
|
||||
void containsTextInFile_range_oneToOne_true() throws IOException {
|
||||
try (PDDocument doc = createDocWithText("findme", "other")) {
|
||||
assertTrue(PdfUtils.containsTextInFile(doc, "findme", "1-1"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("containsTextInFile: list '1,2' finds text (whitespace robust)")
|
||||
void containsTextInFile_list_pages_true() throws IOException {
|
||||
try (PDDocument doc = createDocWithText("foo", "bar")) {
|
||||
assertTrue(PdfUtils.containsTextInFile(doc, "bar", " 1 , 2 "));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("containsTextInFile: text not present -> false")
|
||||
void containsTextInFile_textNotPresent_false() throws IOException {
|
||||
try (PDDocument doc = createDocWithText("xxx", "yyy")) {
|
||||
assertFalse(PdfUtils.containsTextInFile(doc, "zzz", "all"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("pageSize: different size returns false")
|
||||
void pageSize_shouldReturnFalse_whenSizeDoesNotMatch() throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
assertFalse(PdfUtils.pageSize(doc, "600x842"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== New: convertFromPdf – coverage =====================
|
||||
|
||||
@Test
|
||||
@DisplayName("convertFromPdf: singleImage=true creates combined PNG file (readable)")
|
||||
void convertFromPdf_singleImagePng_combinedReadable() throws Exception {
|
||||
// Create two-page PDF
|
||||
byte[] pdfBytes;
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
pdfBytes = baos.toByteArray();
|
||||
}
|
||||
|
||||
byte[] imageBytes =
|
||||
PdfUtils.convertFromPdf(
|
||||
factory, pdfBytes, "png", ImageType.RGB, true, 72, "test.pdf", false);
|
||||
|
||||
// Should be readable as a single combined PNG image
|
||||
BufferedImage img = ImageIO.read(new java.io.ByteArrayInputStream(imageBytes));
|
||||
assertNotNull(img, "PNG should be readable");
|
||||
assertTrue(img.getWidth() > 0 && img.getHeight() > 0, "Image dimensions > 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName(
|
||||
"convertFromPdf: singleImage=false returns ZIP with PNG entries (first image readable)")
|
||||
void convertFromPdf_multiImagePng_firstReadable() throws Exception {
|
||||
// Create two-page PDF
|
||||
byte[] pdfBytes;
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
pdfBytes = baos.toByteArray();
|
||||
}
|
||||
|
||||
// Act: singleImage=false -> ZIP with separate images
|
||||
byte[] zipBytes =
|
||||
PdfUtils.convertFromPdf(
|
||||
factory, pdfBytes, "png", ImageType.RGB, false, 72, "test.pdf", false);
|
||||
|
||||
// Assert: open ZIP, read first entry as PNG
|
||||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
|
||||
ZipEntry entry = zis.getNextEntry();
|
||||
assertNotNull(entry, "ZIP should contain at least one entry");
|
||||
|
||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
||||
zis.transferTo(imgOut);
|
||||
BufferedImage first = ImageIO.read(new ByteArrayInputStream(imgOut.toByteArray()));
|
||||
|
||||
assertNotNull(first, "First PNG entry should be readable");
|
||||
assertTrue(first.getWidth() > 0 && first.getHeight() > 0, "Image dimensions > 0");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasText: detects phrase on selected pages ('1', '2', 'all')")
|
||||
void hasText_shouldDetectPhrase_onSelectedPages() throws Exception {
|
||||
// Arrange: PDF with 2 pages and text
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("alpha on page 1");
|
||||
cs.endText();
|
||||
}
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("beta on page 2");
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasText(doc, "1", "alpha"), "Page 1 should contain 'alpha'");
|
||||
}
|
||||
|
||||
// For further checks, create new doc with identical content
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("alpha on page 1");
|
||||
cs.endText();
|
||||
}
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("beta on page 2");
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasText(doc, "2", "beta"), "Page 2 should contain 'beta'");
|
||||
}
|
||||
|
||||
// Third doc for 'all'
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("gamma");
|
||||
cs.endText();
|
||||
}
|
||||
assertTrue(PdfUtils.hasText(doc, "all", "gamma"), "'all' should find text on page 1");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasTextOnPage: true if page contains phrase, else false")
|
||||
void hasTextOnPage_shouldReturnTrueOnlyForPagesWithPhrase() throws Exception {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 12);
|
||||
cs.newLineAtOffset(50, 750);
|
||||
cs.showText("needle");
|
||||
cs.endText();
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasTextOnPage(p1, "needle"));
|
||||
assertTrue(!PdfUtils.hasTextOnPage(p2, "needle"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasImages: detects images on selected pages and 'all'")
|
||||
void hasImages_shouldDetectImages_onSelectedPages() throws Exception {
|
||||
// Case 1: Page 1 without image (but resources set) -> false
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
p1.setResources(new PDResources());
|
||||
p2.setResources(new PDResources());
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
// Image only on page 2
|
||||
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = bi.createGraphics();
|
||||
g.setColor(Color.GREEN);
|
||||
g.fillRect(0, 0, 20, 20);
|
||||
g.dispose();
|
||||
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(ximg, 50, 700, 20, 20);
|
||||
}
|
||||
|
||||
assertTrue(!PdfUtils.hasImages(doc, "1"), "Page 1 should have no image");
|
||||
}
|
||||
|
||||
// Case 2: Page 2 with image -> true
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
p1.setResources(new PDResources());
|
||||
p2.setResources(new PDResources());
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
BufferedImage bi = new BufferedImage(20, 20, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = bi.createGraphics();
|
||||
g.setColor(Color.BLUE);
|
||||
g.fillRect(0, 0, 20, 20);
|
||||
g.dispose();
|
||||
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p2, AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(ximg, 50, 700, 20, 20);
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasImages(doc, "2"), "Page 2 should have an image");
|
||||
}
|
||||
|
||||
// Case 3: 'all' detects image
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p = new PDPage(PDRectangle.A4);
|
||||
p.setResources(new PDResources());
|
||||
doc.addPage(p);
|
||||
|
||||
BufferedImage bi = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p, AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(ximg, 20, 730, 10, 10);
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasImages(doc, "all"), "'all' should detect the image");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("hasImagesOnPage: true if page contains an image, else false")
|
||||
void hasImagesOnPage_shouldReturnTrueOnlyForPagesWithImage() throws Exception {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p1 = new PDPage(PDRectangle.A4);
|
||||
PDPage p2 = new PDPage(PDRectangle.A4);
|
||||
p1.setResources(new PDResources());
|
||||
p2.setResources(new PDResources());
|
||||
doc.addPage(p1);
|
||||
doc.addPage(p2);
|
||||
|
||||
BufferedImage bi = new BufferedImage(12, 12, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = bi.createGraphics();
|
||||
g.setColor(Color.RED);
|
||||
g.fillRect(0, 0, 12, 12);
|
||||
g.dispose();
|
||||
|
||||
PDImageXObject ximg = LosslessFactory.createFromImage(doc, bi);
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(doc, p1, AppendMode.APPEND, true, true)) {
|
||||
cs.drawImage(ximg, 40, 720, 12, 12);
|
||||
}
|
||||
|
||||
assertTrue(PdfUtils.hasImagesOnPage(p1));
|
||||
assertTrue(!PdfUtils.hasImagesOnPage(p2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("convertFromPdf: singleImage=true with JPG -> no alpha, white background")
|
||||
void convertFromPdf_singleImageJpg_noAlphaWhiteBackground() throws Exception {
|
||||
// small 1-page PDF
|
||||
byte[] pdfBytes;
|
||||
PdfMetadataService meta =
|
||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
PDPage p = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(p);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
doc.save(baos);
|
||||
pdfBytes = baos.toByteArray();
|
||||
}
|
||||
|
||||
byte[] jpgBytes =
|
||||
PdfUtils.convertFromPdf(
|
||||
factory, pdfBytes, "jpg", ImageType.RGB, true, 72, "sample.pdf", false);
|
||||
|
||||
BufferedImage img = ImageIO.read(new ByteArrayInputStream(jpgBytes));
|
||||
assertNotNull(img, "JPG should be readable");
|
||||
|
||||
ColorModel cm = img.getColorModel();
|
||||
assertFalse(cm.hasAlpha(), "JPG output should have no alpha channel");
|
||||
|
||||
// JPG background should be white (approximate check)
|
||||
int rgb = img.getRGB(img.getWidth() / 2, img.getHeight() / 2) & 0x00FFFFFF;
|
||||
assertEquals(0xFFFFFF, rgb, "Background pixel should be white");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user