mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Merge remote-tracking branch 'origin/V2' into mainToV2
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(chmod:*)",
|
||||
"Bash(mkdir:*)",
|
||||
"Bash(./gradlew:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(strings:*)",
|
||||
"Bash(pkill:*)",
|
||||
"Bash(true)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -24,7 +24,7 @@ indent_size = 2
|
||||
insert_final_newline = false
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.js]
|
||||
[{*.js,*.jsx,*.mjs,*.ts,*.tsx}]
|
||||
indent_size = 2
|
||||
|
||||
[*.css]
|
||||
|
||||
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script processes JSON 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.
|
||||
|
||||
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_json.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
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
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.
|
||||
: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).
|
||||
"""
|
||||
if keys is None:
|
||||
keys = {}
|
||||
|
||||
duplicates = []
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def process_dict(obj, current_prefix=""):
|
||||
for key, value in obj.items():
|
||||
full_key = f"{current_prefix}.{key}" if current_prefix else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
process_dict(value, full_key)
|
||||
else:
|
||||
if full_key in keys:
|
||||
duplicates.append((full_key, keys[full_key], full_key))
|
||||
else:
|
||||
keys[full_key] = full_key
|
||||
|
||||
process_dict(data, prefix)
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for JSON files (e.g., 500 KB)
|
||||
MAX_FILE_SIZE = 500 * 1024
|
||||
|
||||
|
||||
def parse_json_file(file_path):
|
||||
"""
|
||||
Parses a JSON translation file and returns a flat dictionary of all keys.
|
||||
:param file_path: Path to the JSON file.
|
||||
:return: Dictionary with flattened keys.
|
||||
"""
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def flatten_dict(d, parent_key="", sep="."):
|
||||
items = {}
|
||||
for k, v in d.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
if isinstance(v, dict):
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
else:
|
||||
items[new_key] = v
|
||||
return items
|
||||
|
||||
return flatten_dict(data)
|
||||
|
||||
|
||||
def unflatten_dict(d, sep="."):
|
||||
"""
|
||||
Converts a flat dictionary with dot notation keys back to nested dict.
|
||||
:param d: Flattened dictionary.
|
||||
:param sep: Separator used in keys.
|
||||
:return: Nested dictionary.
|
||||
"""
|
||||
result = {}
|
||||
for key, value in d.items():
|
||||
parts = key.split(sep)
|
||||
current = result
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
return result
|
||||
|
||||
|
||||
def write_json_file(file_path, updated_properties):
|
||||
"""
|
||||
Writes updated properties back to the JSON file.
|
||||
:param file_path: Path to the JSON 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
|
||||
|
||||
|
||||
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 file_list: List of translation files to update.
|
||||
:param branch: Branch where the files are located.
|
||||
"""
|
||||
reference_properties = parse_json_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 os.path.dirname(file_path).endswith("locales")
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_json_file(os.path.join(branch, file_path))
|
||||
updated_properties = {}
|
||||
|
||||
for ref_key, ref_value in reference_properties.items():
|
||||
if ref_key in current_properties:
|
||||
# Keep the current translation
|
||||
updated_properties[ref_key] = current_properties[ref_key]
|
||||
else:
|
||||
# Add missing key with reference value
|
||||
updated_properties[ref_key] = ref_value
|
||||
|
||||
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_json_keys(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
return parse_json_file(file_path)
|
||||
return {}
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
reference_branch = branch
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_keys = read_json_keys(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(), "frontend", "public", "locales")
|
||||
)
|
||||
|
||||
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))
|
||||
locale_dir = os.path.basename(os.path.dirname(file_normpath))
|
||||
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
and locale_dir == "en-GB"
|
||||
):
|
||||
continue
|
||||
|
||||
if not file_normpath.endswith(".json") or basename_current_file != "translation.json":
|
||||
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))
|
||||
reference_key_count = len(reference_keys)
|
||||
current_key_count = len(current_keys)
|
||||
|
||||
if reference_key_count != current_key_count:
|
||||
report.append("")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
has_differences = True
|
||||
if reference_key_count > current_key_count:
|
||||
report.append(
|
||||
f" - **_Mismatched key count_**: {reference_key_count} (reference) vs {current_key_count} (current). Translation keys are missing."
|
||||
)
|
||||
elif reference_key_count < current_key_count:
|
||||
report.append(
|
||||
f" - **_Too many keys_**: {reference_key_count} (reference) vs {current_key_count} (current). Please verify if there are additional keys that need to be removed."
|
||||
)
|
||||
else:
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys_set = set(current_keys.keys())
|
||||
reference_keys_set = set(reference_keys.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:
|
||||
report.append(
|
||||
f" - **_Extra keys in `{locale_dir}/{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{locale_dir}/{basename_current_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_reference_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 {first}, duplicate at `{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 [en-GB/translation.json](https://github.com/Stirling-Tools/Stirling-PDF/blob/V2/frontend/public/locales/en-GB/translation.json)"
|
||||
)
|
||||
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(),
|
||||
"frontend",
|
||||
"public",
|
||||
"locales",
|
||||
"*",
|
||||
"translation.json",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
@@ -0,0 +1,501 @@
|
||||
name: Auto PR V2 Deployment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: "PR number to deploy"
|
||||
required: true
|
||||
allow_fork:
|
||||
description: "Allow deploying fork PR?"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-pr:
|
||||
if: (github.event_name == 'pull_request' && github.event.action != 'closed') || github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_deploy: ${{ steps.decide.outputs.should_deploy }}
|
||||
is_fork: ${{ steps.resolve.outputs.is_fork }}
|
||||
allow_fork: ${{ steps.decide.outputs.allow_fork }}
|
||||
pr_number: ${{ steps.resolve.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.resolve.outputs.repository }}
|
||||
pr_ref: ${{ steps.resolve.outputs.ref }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Resolve PR info
|
||||
id: resolve
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
let prNumber = context.eventName === 'workflow_dispatch'
|
||||
? parseInt(process.env.INPUT_PR, 10)
|
||||
: context.payload.number;
|
||||
|
||||
if (!Number.isInteger(prNumber)) { core.setFailed('Invalid PR number'); return; }
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||
core.setOutput('pr_number', String(prNumber));
|
||||
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);
|
||||
|
||||
- name: Decide deploy
|
||||
id: decide
|
||||
shell: bash
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
STATE: ${{ steps.resolve.outputs.state }}
|
||||
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
|
||||
# Standard: nichts deployen
|
||||
should=false
|
||||
allow_fork="$(echo "${ALLOW_FORK_INPUT:-false}" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
if [ "$STATE" != "open" ]; then
|
||||
echo "PR not open -> skip"
|
||||
else
|
||||
if [ "$IS_FORK" = "true" ] && [ "$allow_fork" != "true" ]; then
|
||||
echo "Fork PR and allow_fork=false -> skip"
|
||||
else
|
||||
should=true
|
||||
fi
|
||||
fi
|
||||
else
|
||||
auth_users=("Frooodle" "sf298" "Ludy87" "LaserKaspar" "sbplat" "reecebrowne" "DarioGii" "ConnorYoh" "EthanHealy01" "jbrunton96")
|
||||
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
|
||||
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
|
||||
|
||||
echo "should_deploy=$should" >> $GITHUB_OUTPUT
|
||||
echo "allow_fork=${allow_fork:-false}" >> $GITHUB_OUTPUT
|
||||
|
||||
deploy-v2-pr:
|
||||
needs: check-pr
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check-pr.outputs.should_deploy == 'true' && (needs.check-pr.outputs.is_fork == 'false' || needs.check-pr.outputs.allow_fork == 'true')
|
||||
# Concurrency control - only one deployment per PR at a time
|
||||
concurrency:
|
||||
group: v2-deploy-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
cancel-in-progress: true
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: main
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Add deployment started comment
|
||||
id: deployment-started
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
|
||||
// Delete previous V2 deployment comments to avoid clutter
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(comment =>
|
||||
comment.body.includes('🚀 **Auto-deploying V2 version**') ||
|
||||
comment.body.includes('## 🚀 V2 Auto-Deployment Complete!') ||
|
||||
comment.body.includes('❌ **V2 Auto-deployment failed**')
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
console.log(`Deleting old V2 comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
// Create new deployment started comment
|
||||
const { data: newComment } = await github.rest.issues.createComment({
|
||||
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.`
|
||||
});
|
||||
return newComment.id;
|
||||
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: ${{ needs.check-pr.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-pr.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0 # Fetch full history for commit hash detection
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: |
|
||||
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
|
||||
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Build and push V2 frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push V2 backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy V2 to VPS
|
||||
id: deploy
|
||||
run: |
|
||||
# Use same port strategy as regular PRs - just the PR number
|
||||
V2_PORT=${{ needs.check-pr.outputs.pr_number }}
|
||||
BACKEND_PORT=$((V2_PORT + 10000)) # Backend on higher port to avoid conflicts
|
||||
|
||||
# Create docker-compose for V2 with separate frontend and backend
|
||||
cat > docker-compose.yml << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf-v2-backend:
|
||||
container_name: stirling-pdf-v2-backend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "${BACKEND_PORT}:8080" # Backend API port
|
||||
volumes:
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Frontend/Backend Split Architecture"
|
||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
baseUrl: "https://${V2_PORT}.ssl.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
stirling-pdf-v2-frontend:
|
||||
container_name: stirling-pdf-v2-frontend-pr-${{ needs.check-pr.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "${V2_PORT}:80" # Frontend port (same as regular PRs)
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:${BACKEND_PORT}"
|
||||
depends_on:
|
||||
- stirling-pdf-v2-backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Deploy to VPS
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose-v2.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
# Create V2 PR-specific directories
|
||||
mkdir -p /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose-v2.yml /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Stop any existing container and clean up
|
||||
cd /stirling/V2-PR-${{ needs.check-pr.outputs.pr_number }}
|
||||
docker-compose down --remove-orphans 2>/dev/null || true
|
||||
|
||||
# Start the new container
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
|
||||
# Clean up unused Docker resources to save space
|
||||
docker system prune -af --volumes || true
|
||||
|
||||
# Clean up old backend/frontend images (older than 2 weeks)
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
# Set port for output
|
||||
echo "v2_port=${V2_PORT}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post V2 deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ needs.check-pr.outputs.pr_number }};
|
||||
const v2Port = ${{ steps.deploy.outputs.v2_port }};
|
||||
|
||||
// Delete the "deploying..." comment since we're posting the final result
|
||||
const deploymentStartedId = ${{ steps.deployment-started.outputs.result }};
|
||||
if (deploymentStartedId) {
|
||||
console.log(`Deleting deployment started comment: ${deploymentStartedId}`);
|
||||
try {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: deploymentStartedId
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Could not delete deployment started comment: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${v2Port}`;
|
||||
const httpsUrl = `https://${v2Port}.ssl.stirlingpdf.cloud`;
|
||||
|
||||
const commentBody = `## 🚀 V2 Auto-Deployment Complete!\n\n` +
|
||||
`Your V2 PR with the new frontend/backend split architecture has been deployed!\n\n` +
|
||||
`🔗 **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.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
cleanup-v2-deployment:
|
||||
if: github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
continue-on-error: true
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Clean up V2 deployment comments
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = ${{ github.event.pull_request.number }};
|
||||
|
||||
// Find and delete V2 deployment comments
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber
|
||||
});
|
||||
|
||||
const v2Comments = comments.filter(c =>
|
||||
c.body?.includes("## 🚀 V2 Auto-Deployment Complete!") &&
|
||||
c.user?.type === "Bot"
|
||||
);
|
||||
|
||||
for (const comment of v2Comments) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
console.log(`Deleted V2 deployment comment (ID: ${comment.id})`);
|
||||
}
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup V2 deployment
|
||||
run: |
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/V2-PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found V2 PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove V2 containers
|
||||
cd /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove V2 PR-specific directories
|
||||
rm -rf /stirling/V2-PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Clean up V2 containers by name (in case compose cleanup missed them)
|
||||
docker rm -f stirling-pdf-v2-frontend-pr-${{ github.event.pull_request.number }} || true
|
||||
docker rm -f stirling-pdf-v2-backend-pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "V2 cleanup completed"
|
||||
else
|
||||
echo "V2 PR directory not found, nothing to clean up"
|
||||
fi
|
||||
|
||||
# Clean up old unused images (older than 2 weeks) but keep recent ones for reuse
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
|
||||
# Note: We don't remove the commit-based images since they can be reused across PRs
|
||||
# Only remove PR-specific containers and directories
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
continue-on-error: true
|
||||
@@ -29,6 +29,7 @@ jobs:
|
||||
github.event.comment.user.login == 'reecebrowne' ||
|
||||
github.event.comment.user.login == 'DarioGii' ||
|
||||
github.event.comment.user.login == 'EthanHealy01' ||
|
||||
github.event.comment.user.login == 'jbrunton96' ||
|
||||
github.event.comment.user.login == 'ConnorYoh'
|
||||
)
|
||||
outputs:
|
||||
@@ -44,7 +45,11 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
<<<<<<< HEAD
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
=======
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -132,7 +137,11 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR
|
||||
<<<<<<< HEAD
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
=======
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
@@ -144,7 +153,11 @@ jobs:
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout PR
|
||||
<<<<<<< HEAD
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
=======
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
with:
|
||||
ref: refs/pull/${{ needs.check-comment.outputs.pr_number }}/merge
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
+50
-47
@@ -1,11 +1,9 @@
|
||||
name: Build and Test Workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# branches: ["main"]
|
||||
pull_request:
|
||||
branches: ["main"]
|
||||
branches: ["main", "V2", "V2-gha"]
|
||||
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
|
||||
@@ -27,7 +25,6 @@ jobs:
|
||||
name: detect what files changed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
# Map a step output to a job output
|
||||
outputs:
|
||||
build: ${{ steps.changes.outputs.build }}
|
||||
app: ${{ steps.changes.outputs.app }}
|
||||
@@ -37,29 +34,26 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Check for file changes
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
uses: dorny/[email protected]
|
||||
id: changes
|
||||
with:
|
||||
filters: ".github/config/.files.yaml"
|
||||
filters: .github/config/.files.yaml
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [17, 21]
|
||||
spring-security: [true, false]
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
@@ -68,19 +62,15 @@ jobs:
|
||||
with:
|
||||
java-version: ${{ matrix.jdk-version }}
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
with:
|
||||
gradle-version: 8.14
|
||||
|
||||
- name: Build with Gradle and spring security ${{ matrix.spring-security }}
|
||||
run: ./gradlew clean build
|
||||
run: ./gradlew clean build -PnoSpotless
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: ${{ matrix.spring-security }}
|
||||
|
||||
- name: Check Test Reports Exist
|
||||
id: check-reports
|
||||
if: always()
|
||||
run: |
|
||||
declare -a dirs=(
|
||||
@@ -91,41 +81,28 @@ jobs:
|
||||
"app/proprietary/build/reports/tests/"
|
||||
"app/proprietary/build/test-results/"
|
||||
)
|
||||
missing_reports=()
|
||||
for dir in "${dirs[@]}"; do
|
||||
if [ ! -d "$dir" ]; then
|
||||
missing_reports+=("$dir")
|
||||
echo "Missing $dir"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
if [ ${#missing_reports[@]} -gt 0 ]; then
|
||||
echo "ERROR: The following required test report directories are missing:"
|
||||
printf '%s\n' "${missing_reports[@]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All required test report directories are present"
|
||||
|
||||
- name: Upload Test Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
name: test-reports-jdk-${{ matrix.jdk-version }}-spring-security-${{ matrix.spring-security }}
|
||||
path: |
|
||||
app/core/build/reports/tests/
|
||||
app/core/build/test-results/
|
||||
app/core/build/reports/problems/
|
||||
app/common/build/reports/tests/
|
||||
app/common/build/test-results/
|
||||
app/common/build/reports/problems/
|
||||
app/proprietary/build/reports/tests/
|
||||
app/proprietary/build/test-results/
|
||||
app/proprietary/build/reports/problems/
|
||||
app/**/build/reports/tests/
|
||||
app/**/build/test-results/
|
||||
app/**/build/reports/problems/
|
||||
build/reports/problems/
|
||||
retention-days: 3
|
||||
if-no-files-found: warn
|
||||
|
||||
check-generateOpenApiDocs:
|
||||
if: needs.files-changed.outputs.openapi == 'true'
|
||||
needs: [files-changed, build]
|
||||
needs: [files-changed]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -151,11 +128,41 @@ jobs:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Upload OpenAPI Documentation
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
name: openapi-docs
|
||||
path: ./SwaggerDoc.json
|
||||
|
||||
frontend-validation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/[email protected]
|
||||
with:
|
||||
egress-policy: audit
|
||||
- name: Checkout repository
|
||||
uses: actions/[email protected]
|
||||
- name: Set up Node.js
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- name: Install frontend dependencies
|
||||
run: cd frontend && npm ci
|
||||
- name: Lint frontend
|
||||
run: cd frontend && npm run lint
|
||||
- name: Build frontend
|
||||
run: cd frontend && npm run build
|
||||
- name: Run frontend tests
|
||||
run: cd frontend && npm run test -- --run
|
||||
- name: Upload frontend build artifacts
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
name: frontend-build
|
||||
path: frontend/dist/
|
||||
retention-days: 3
|
||||
|
||||
check-licence:
|
||||
if: needs.files-changed.outputs.build == 'true'
|
||||
needs: [files-changed, build]
|
||||
@@ -165,7 +172,6 @@ jobs:
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
@@ -175,19 +181,15 @@ jobs:
|
||||
java-version: "17"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Check licenses for compatibility
|
||||
- name: check the licenses for compatibility
|
||||
run: ./gradlew clean checkLicense
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: false
|
||||
STIRLING_PDF_DESKTOP_UI: true
|
||||
|
||||
- name: FAILED - Check licenses for compatibility
|
||||
- name: FAILED - check the licenses for compatibility
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/[email protected]
|
||||
with:
|
||||
name: dependencies-without-allowed-license.json
|
||||
path: |
|
||||
build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
path: build/reports/dependency-license/dependencies-without-allowed-license.json
|
||||
retention-days: 3
|
||||
|
||||
docker-compose-tests:
|
||||
@@ -218,6 +220,7 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
|
||||
- name: Set up Java 17
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
@@ -296,7 +299,7 @@ jobs:
|
||||
with:
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
context: .
|
||||
file: ./${{ matrix.docker-rev }}
|
||||
file: ./docker/backend/${{ matrix.docker-rev }}
|
||||
push: false
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
name: Auto V2 Deploy on Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- deploy-on-v2-commit
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy-v2-on-push:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: deploy-v2-push-V2
|
||||
cancel-in-progress: true
|
||||
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get commit hashes for frontend and backend
|
||||
id: commit-hashes
|
||||
run: |
|
||||
# Get last commit that touched the frontend folder, docker/frontend, or docker/compose
|
||||
FRONTEND_HASH=$(git log -1 --format="%H" -- frontend/ docker/frontend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$FRONTEND_HASH" ]; then
|
||||
FRONTEND_HASH="no-frontend-changes"
|
||||
fi
|
||||
|
||||
# Get last commit that touched backend code, docker/backend, or docker/compose
|
||||
BACKEND_HASH=$(git log -1 --format="%H" -- app/ docker/backend/ docker/compose/ 2>/dev/null || echo "")
|
||||
if [ -z "$BACKEND_HASH" ]; then
|
||||
BACKEND_HASH="no-backend-changes"
|
||||
fi
|
||||
|
||||
echo "Frontend hash: $FRONTEND_HASH"
|
||||
echo "Backend hash: $BACKEND_HASH"
|
||||
|
||||
echo "frontend_hash=$FRONTEND_HASH" >> $GITHUB_OUTPUT
|
||||
echo "backend_hash=$BACKEND_HASH" >> $GITHUB_OUTPUT
|
||||
|
||||
# Short hashes for tags
|
||||
if [ "$FRONTEND_HASH" = "no-frontend-changes" ]; then
|
||||
echo "frontend_short=no-frontend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "frontend_short=${FRONTEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_HASH" = "no-backend-changes" ]; then
|
||||
echo "backend_short=no-backend" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "backend_short=${BACKEND_HASH:0:8}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Check if frontend image exists
|
||||
id: check-frontend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Frontend image needs to be built"
|
||||
fi
|
||||
|
||||
- name: Check if backend image exists
|
||||
id: check-backend
|
||||
run: |
|
||||
if docker manifest inspect ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }} >/dev/null 2>&1; then
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Backend image already exists, skipping build"
|
||||
else
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "Backend image needs to be built"
|
||||
fi
|
||||
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push frontend image
|
||||
if: steps.check-frontend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/frontend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build and push backend image
|
||||
if: steps.check-backend.outputs.exists == 'false'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/backend/Dockerfile
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-latest
|
||||
build-args: VERSION_TAG=v2-alpha
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
chmod 600 ../private.key
|
||||
|
||||
|
||||
- name: Deploy to VPS on port 3000
|
||||
run: |
|
||||
export UNIQUE_NAME=docker-compose-v2-$GITHUB_RUN_ID.yml
|
||||
|
||||
cat > $UNIQUE_NAME << EOF
|
||||
version: '3.3'
|
||||
services:
|
||||
backend:
|
||||
container_name: stirling-v2-backend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-backend-${{ steps.commit-hashes.outputs.backend_short }}
|
||||
ports:
|
||||
- "13000:8080"
|
||||
volumes:
|
||||
- /stirling/V2/data:/usr/share/tessdata:rw
|
||||
- /stirling/V2/config:/configs:rw
|
||||
- /stirling/V2/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF V2"
|
||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
SWAGGER_SERVER_URL: "https://demo.stirlingpdf.cloud"
|
||||
baseUrl: "https://demo.stirlingpdf.cloud"
|
||||
restart: on-failure:5
|
||||
|
||||
frontend:
|
||||
container_name: stirling-v2-frontend
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:v2-frontend-${{ steps.commit-hashes.outputs.frontend_short }}
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
VITE_API_BASE_URL: "http://${{ secrets.VPS_HOST }}:13000"
|
||||
depends_on:
|
||||
- backend
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Copy to remote with unique name
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no $UNIQUE_NAME ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/$UNIQUE_NAME
|
||||
|
||||
# SSH and rename/move atomically to avoid interference
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
|
||||
mkdir -p /stirling/V2/{data,config,logs}
|
||||
mv /tmp/$UNIQUE_NAME /stirling/V2/docker-compose.yml
|
||||
cd /stirling/V2
|
||||
docker-compose down || true
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker system prune -af --volumes || true
|
||||
docker image prune -af --filter "until=336h" --filter "label!=keep=true" || true
|
||||
ENDSSH
|
||||
|
||||
- name: Cleanup temporary files
|
||||
if: always()
|
||||
run: |
|
||||
rm -f ../private.key
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
name: Frontend License Report Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
pull_request:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- ".github/workflows/frontend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
generate-frontend-license-report:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false)
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: frontend
|
||||
env:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
run: npm ci --ignore-scripts --audit=false --fund=false
|
||||
|
||||
- name: Generate frontend license report (internal PR)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
working-directory: frontend
|
||||
env:
|
||||
PR_IS_FORK: "false"
|
||||
run: npm run generate-licenses
|
||||
|
||||
- name: Generate frontend license report (fork PRs, pinned)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
env:
|
||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p src/assets
|
||||
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Postprocess with project script (BASE version)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
env:
|
||||
PR_IS_FORK: "true"
|
||||
run: |
|
||||
node base/frontend/scripts/generate-licenses.js \
|
||||
--input frontend/src/assets/3rdPartyLicenses.json
|
||||
|
||||
- name: Copy postprocessed artifacts back (fork PRs)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
run: |
|
||||
mkdir -p frontend/src/assets
|
||||
if [ -f "base/frontend/src/assets/3rdPartyLicenses.json" ]; then
|
||||
cp base/frontend/src/assets/3rdPartyLicenses.json frontend/src/assets/3rdPartyLicenses.json
|
||||
fi
|
||||
if [ -f "base/frontend/src/assets/license-warnings.json" ]; then
|
||||
cp base/frontend/src/assets/license-warnings.json frontend/src/assets/license-warnings.json
|
||||
fi
|
||||
|
||||
- name: Check for license warnings
|
||||
run: |
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo "LICENSE_WARNINGS_EXIST=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "LICENSE_WARNINGS_EXIST=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
// Get all comments on the PR
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
// Filter for license check comments
|
||||
const licenseComments = comments.filter(comment =>
|
||||
comment.body.includes('## ✅ Frontend License Check Passed') ||
|
||||
comment.body.includes('## ❌ Frontend License Check Failed')
|
||||
);
|
||||
|
||||
// Delete old license check comments
|
||||
for (const comment of licenseComments) {
|
||||
console.log(`Deleting old license check comment: ${comment.id}`);
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: comment.id
|
||||
});
|
||||
}
|
||||
|
||||
- name: Summarize results (fork PRs)
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||
run: |
|
||||
{
|
||||
echo "## Frontend License Check"
|
||||
echo ""
|
||||
if [ "${LICENSE_WARNINGS_EXIST}" = "true" ]; then
|
||||
echo "❌ **Failed** – incompatible or unknown licenses found."
|
||||
if [ -f "frontend/src/assets/license-warnings.json" ]; then
|
||||
echo ""
|
||||
echo "### Warnings"
|
||||
jq -r '.warnings[] | "- \(.message)"' frontend/src/assets/license-warnings.json || true
|
||||
fi
|
||||
else
|
||||
echo "✅ **Passed** – no license warnings detected."
|
||||
fi
|
||||
echo ""
|
||||
echo "_Note: This is a fork PR. PR comments are disabled; use this summary._"
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.issue.number;
|
||||
const hasWarnings = process.env.LICENSE_WARNINGS_EXIST === 'true';
|
||||
|
||||
let commentBody;
|
||||
|
||||
if (hasWarnings) {
|
||||
// Read warnings file to get specific issues
|
||||
const fs = require('fs');
|
||||
let warningDetails = '';
|
||||
try {
|
||||
const warnings = JSON.parse(fs.readFileSync('frontend/src/assets/license-warnings.json', 'utf8'));
|
||||
warningDetails = warnings.warnings.map(w => `- ${w.message}`).join('\n');
|
||||
} catch (e) {
|
||||
warningDetails = 'Unable to read warning details';
|
||||
}
|
||||
|
||||
commentBody = `## ❌ Frontend License Check Failed
|
||||
|
||||
The frontend license check has detected compatibility warnings that require review:
|
||||
|
||||
${warningDetails}
|
||||
|
||||
**Action Required:** Please review these licenses to ensure they are acceptable for your use case before merging.
|
||||
|
||||
_This check will fail the PR until license issues are resolved._`;
|
||||
} else {
|
||||
commentBody = `## ✅ Frontend License Check Passed
|
||||
|
||||
All frontend licenses have been validated and no compatibility warnings were detected.
|
||||
|
||||
The frontend license report has been updated successfully.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
- name: Fail workflow if license warnings exist (PR only)
|
||||
if: github.event_name == 'pull_request' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: |
|
||||
echo "❌ License warnings detected. Failing the workflow."
|
||||
exit 1
|
||||
|
||||
# Push Event: Commit license files and create PR
|
||||
- name: Commit changes (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
git add frontend/src/assets/3rdPartyLicenses.json
|
||||
# Note: Do NOT commit license-warnings.json - it's only for PR review
|
||||
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
|
||||
- name: Prepare PR body (Push only)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
PR_BODY="Auto-generated by ${{ steps.setup-bot.outputs.app-slug }}[bot]
|
||||
|
||||
This PR updates the frontend license report based on changes to package.json dependencies."
|
||||
|
||||
if [ "${{ env.LICENSE_WARNINGS_EXIST }}" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
|
||||
## ⚠️ License Compatibility Warnings
|
||||
|
||||
The following licenses may require review for corporate compatibility:
|
||||
|
||||
$(cat frontend/src/assets/license-warnings.json | jq -r '.warnings[].message')
|
||||
|
||||
Please review these licenses to ensure they are acceptable for your use case."
|
||||
fi
|
||||
|
||||
echo "PR_BODY<<EOF" >> $GITHUB_ENV
|
||||
echo "$PR_BODY" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
|
||||
- name: Create Pull Request (Push only)
|
||||
id: cpr
|
||||
if: github.event_name == 'push' && env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: "Update Frontend 3rd Party Licenses"
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: update-frontend-3rd-party-licenses
|
||||
base: V2
|
||||
title: "Update Frontend 3rd Party Licenses"
|
||||
body: ${{ env.PR_BODY }}
|
||||
labels: Licenses,github-actions,frontend
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
- name: Enable Pull Request Automerge (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'false'
|
||||
run: gh pr merge --squash --auto "${{ steps.cpr.outputs.pull-request-number }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
|
||||
- name: Add review required label (Push only)
|
||||
if: github.event_name == 'push' && steps.cpr.outputs.pull-request-operation == 'created' && env.LICENSE_WARNINGS_EXIST == 'true'
|
||||
run: gh pr edit "${{ steps.cpr.outputs.pull-request-number }}" --add-label "license-review-required"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.setup-bot.outputs.token }}
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Sync Files V2
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- syncLangTest
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.json"
|
||||
- "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
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.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@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Sync translation JSON files
|
||||
run: |
|
||||
python .github/scripts/check_language_json.py --reference-file "frontend/public/locales/en-GB/translation.json" --branch V2
|
||||
|
||||
- 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"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation_v2.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_v2
|
||||
base: V2
|
||||
title: ":globe_with_meridians: [V2] 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:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`.
|
||||
- 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
|
||||
frontend/public/locales/*/translation.json
|
||||
+10
@@ -27,6 +27,7 @@ clientWebUI/
|
||||
!cucumber/exampleFiles/
|
||||
!cucumber/exampleFiles/example_html.zip
|
||||
exampleYmlFiles/stirling/
|
||||
/stirling/
|
||||
/testing/file_snapshots
|
||||
SwaggerDoc.json
|
||||
|
||||
@@ -196,6 +197,8 @@ id_ed25519.pub
|
||||
.pytest_cache
|
||||
.ipynb_checkpoints
|
||||
|
||||
|
||||
|
||||
**/jcef-bundle/
|
||||
|
||||
# node_modules
|
||||
@@ -203,3 +206,10 @@ node_modules/
|
||||
|
||||
# weasyPrint
|
||||
**/LOCAL_APPDATA_FONTCONFIG_CACHE/**
|
||||
|
||||
# Translation temp files
|
||||
*_compact.json
|
||||
*compact*.json
|
||||
test_batch.json
|
||||
*.backup.*.json
|
||||
frontend/public/locales/*/translation.backup*.json
|
||||
|
||||
Vendored
+1
@@ -19,5 +19,6 @@
|
||||
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
|
||||
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
|
||||
"dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+4
-1
@@ -139,5 +139,8 @@
|
||||
"app/core/src/main/java",
|
||||
"app/common/src/main/java",
|
||||
"app/proprietary/src/main/java"
|
||||
]
|
||||
],
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
+300
@@ -0,0 +1,300 @@
|
||||
# Adding New React Tools to Stirling PDF
|
||||
|
||||
This guide covers how to add new PDF tools to the React frontend, either by migrating existing Thymeleaf templates or creating entirely new tools.
|
||||
|
||||
## Overview
|
||||
|
||||
When adding tools, follow this systematic approach using the established patterns and architecture.
|
||||
|
||||
## 1. Create Tool Structure
|
||||
|
||||
Create these files in the correct directories:
|
||||
```
|
||||
frontend/src/hooks/tools/[toolName]/
|
||||
├── use[ToolName]Parameters.ts # Parameter definitions and validation
|
||||
└── use[ToolName]Operation.ts # Tool operation logic using useToolOperation
|
||||
|
||||
frontend/src/components/tools/[toolName]/
|
||||
└── [ToolName]Settings.tsx # Settings UI component (if needed)
|
||||
|
||||
frontend/src/tools/
|
||||
└── [ToolName].tsx # Main tool component
|
||||
```
|
||||
|
||||
## 2. Implementation Pattern
|
||||
|
||||
Use `useBaseTool` for simplified hook management. This is the recommended approach for all new tools:
|
||||
|
||||
**Parameters Hook** (`use[ToolName]Parameters.ts`):
|
||||
```typescript
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export interface [ToolName]Parameters extends BaseParameters {
|
||||
// Define your tool-specific parameters here
|
||||
someOption: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: [ToolName]Parameters = {
|
||||
someOption: false,
|
||||
};
|
||||
|
||||
export const use[ToolName]Parameters = (): BaseParametersHook<[ToolName]Parameters> => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: 'your-endpoint-name',
|
||||
validateFn: (params) => true, // Add validation logic
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Operation Hook** (`use[ToolName]Operation.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
|
||||
export const build[ToolName]FormData = (parameters: [ToolName]Parameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
// Add parameters to formData
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const [toolName]OperationConfig = {
|
||||
toolType: ToolType.singleFile, // or ToolType.multiFile (buildFormData's file parameter will need to be updated)
|
||||
buildFormData: build[ToolName]FormData,
|
||||
operationType: '[toolName]',
|
||||
endpoint: '/api/v1/category/endpoint-name',
|
||||
filePrefix: 'processed_', // Will be overridden with translation
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const use[ToolName]Operation = () => {
|
||||
const { t } = useTranslation();
|
||||
return useToolOperation({
|
||||
...[toolName]OperationConfig,
|
||||
filePrefix: t('[toolName].filenamePrefix', 'processed') + '_',
|
||||
getErrorMessage: createStandardErrorHandler(t('[toolName].error.failed', 'Operation failed'))
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
**Main Component** (`[ToolName].tsx`):
|
||||
```typescript
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createToolFlow } from "../components/tools/shared/createToolFlow";
|
||||
import { use[ToolName]Parameters } from "../hooks/tools/[toolName]/use[ToolName]Parameters";
|
||||
import { use[ToolName]Operation } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||
import { useBaseTool } from "../hooks/tools/shared/useBaseTool";
|
||||
import { BaseToolProps, ToolComponent } from "../types/tool";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const { t } = useTranslation();
|
||||
const base = useBaseTool('[toolName]', use[ToolName]Parameters, use[ToolName]Operation, props);
|
||||
|
||||
return createToolFlow({
|
||||
files: {
|
||||
selectedFiles: base.selectedFiles,
|
||||
isCollapsed: base.hasResults,
|
||||
placeholder: t("[toolName].files.placeholder", "Select files to get started"),
|
||||
},
|
||||
steps: [
|
||||
// Add settings steps if needed
|
||||
],
|
||||
executeButton: {
|
||||
text: t("[toolName].submit", "Process"),
|
||||
isVisible: !base.hasResults,
|
||||
loadingText: t("loading"),
|
||||
onClick: base.handleExecute,
|
||||
disabled: !base.params.validateParameters() || !base.hasFiles || !base.endpointEnabled,
|
||||
},
|
||||
review: {
|
||||
isVisible: base.hasResults,
|
||||
operation: base.operation,
|
||||
title: t("[toolName].results.title", "Results"),
|
||||
onFileClick: base.handleThumbnailClick,
|
||||
onUndo: base.handleUndo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
[ToolName].tool = () => use[ToolName]Operation;
|
||||
export default [ToolName] as ToolComponent;
|
||||
```
|
||||
|
||||
**Note**: Some existing tools (like AddPassword, Compress) use a legacy pattern with manual hook management. **Always use the Modern Pattern above for new tools** - it's cleaner, more maintainable, and includes automation support.
|
||||
|
||||
## 3. Register Tool in System
|
||||
Update these files to register your new tool:
|
||||
|
||||
**Tool Registry** (`frontend/src/data/useTranslatedToolRegistry.tsx`):
|
||||
1. Add imports at the top:
|
||||
```typescript
|
||||
import [ToolName] from "../tools/[ToolName]";
|
||||
import { [toolName]OperationConfig } from "../hooks/tools/[toolName]/use[ToolName]Operation";
|
||||
import [ToolName]Settings from "../components/tools/[toolName]/[ToolName]Settings";
|
||||
```
|
||||
|
||||
2. Add tool entry in the `allTools` object:
|
||||
```typescript
|
||||
[toolName]: {
|
||||
icon: <LocalIcon icon="your-icon-name" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.[toolName].title", "Tool Name"),
|
||||
component: [ToolName],
|
||||
description: t("home.[toolName].desc", "Tool description"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS, // or appropriate category
|
||||
subcategoryId: SubcategoryId.APPROPRIATE_SUBCATEGORY,
|
||||
maxFiles: -1, // or specific number
|
||||
endpoints: ["endpoint-name"],
|
||||
operationConfig: [toolName]OperationConfig,
|
||||
settingsComponent: [ToolName]Settings, // if settings exist
|
||||
},
|
||||
```
|
||||
|
||||
## 4. Add Tooltips (Optional but Recommended)
|
||||
Create user-friendly tooltips to help non-technical users understand your tool. **Use simple, clear language - avoid technical jargon:**
|
||||
|
||||
**Tooltip Hook** (`frontend/src/components/tooltips/use[ToolName]Tips.ts`):
|
||||
```typescript
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TooltipContent } from '../../types/tips';
|
||||
|
||||
export const use[ToolName]Tips = (): TooltipContent => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
header: {
|
||||
title: t("[toolName].tooltip.header.title", "Tool Overview")
|
||||
},
|
||||
tips: [
|
||||
{
|
||||
title: t("[toolName].tooltip.description.title", "What does this tool do?"),
|
||||
description: t("[toolName].tooltip.description.text", "Simple explanation in everyday language that non-technical users can understand."),
|
||||
bullets: [
|
||||
t("[toolName].tooltip.description.bullet1", "Easy-to-understand benefit 1"),
|
||||
t("[toolName].tooltip.description.bullet2", "Easy-to-understand benefit 2")
|
||||
]
|
||||
}
|
||||
// Add more tip sections as needed
|
||||
]
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Add tooltip to your main component:**
|
||||
```typescript
|
||||
import { use[ToolName]Tips } from "../components/tooltips/use[ToolName]Tips";
|
||||
|
||||
const [ToolName] = (props: BaseToolProps) => {
|
||||
const tips = use[ToolName]Tips();
|
||||
|
||||
// In your steps array:
|
||||
steps: [
|
||||
{
|
||||
title: t("[toolName].steps.settings", "Settings"),
|
||||
tooltip: tips, // Add this line
|
||||
content: <[ToolName]Settings ... />
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## 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`
|
||||
|
||||
**Required Translation Keys**:
|
||||
```json
|
||||
{
|
||||
"home": {
|
||||
"[toolName]": {
|
||||
"title": "Tool Name",
|
||||
"desc": "Tool description"
|
||||
}
|
||||
},
|
||||
"[toolName]": {
|
||||
"title": "Tool Name",
|
||||
"submit": "Process",
|
||||
"filenamePrefix": "processed",
|
||||
"files": {
|
||||
"placeholder": "Select files to get started"
|
||||
},
|
||||
"steps": {
|
||||
"settings": "Settings"
|
||||
},
|
||||
"options": {
|
||||
"title": "Tool Options",
|
||||
"someOption": "Option Label",
|
||||
"someOption.desc": "Option description",
|
||||
"note": "General information about the tool."
|
||||
},
|
||||
"results": {
|
||||
"title": "Results"
|
||||
},
|
||||
"error": {
|
||||
"failed": "Operation failed"
|
||||
},
|
||||
"tooltip": {
|
||||
"header": {
|
||||
"title": "Tool Overview"
|
||||
},
|
||||
"description": {
|
||||
"title": "What does this tool do?",
|
||||
"text": "Simple explanation in everyday language",
|
||||
"bullet1": "Easy-to-understand benefit 1",
|
||||
"bullet2": "Easy-to-understand benefit 2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Translation Notes:**
|
||||
- **Only update `en-GB/translation.json`** - 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
|
||||
|
||||
**Tooltip Writing Guidelines:**
|
||||
- **Use simple, everyday language** - avoid technical terms like "converts interactive elements"
|
||||
- **Focus on benefits** - explain what the user gains, not how it works internally
|
||||
- **Use concrete examples** - "text boxes become regular text" vs "form fields are flattened"
|
||||
- **Answer user questions** - "What does this do?", "When should I use this?", "What's this option for?"
|
||||
- **Keep descriptions concise** - 1-2 sentences maximum per section
|
||||
- **Use bullet points** for multiple benefits or features
|
||||
|
||||
## 6. Migration from Thymeleaf
|
||||
When migrating existing Thymeleaf templates:
|
||||
|
||||
1. **Identify Form Parameters**: Look at the original `<form>` inputs to determine parameter structure
|
||||
2. **Extract Translation Keys**: Find `#{key.name}` references and add them to JSON translations (For many tools these translations will already exist but some parts will be missing)
|
||||
3. **Map API Endpoint**: Note the `th:action` URL for the operation hook
|
||||
4. **Preserve Functionality**: Ensure all original form behaviour is replicated which is applicable to V2 react UI
|
||||
|
||||
## 7. Testing Your Tool
|
||||
- Verify tool appears in UI with correct icon and description
|
||||
- Test with various file sizes and types
|
||||
- Confirm translations work
|
||||
- Check error handling
|
||||
- Test undo functionality
|
||||
- Verify results display correctly
|
||||
|
||||
## Tool Development Patterns
|
||||
|
||||
### Three Tool Patterns:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
@@ -0,0 +1,228 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Build and Test
|
||||
- **Build project**: `./gradlew clean build`
|
||||
- **Run locally**: `./gradlew bootRun`
|
||||
- **Full test suite**: `./test.sh` (builds all Docker variants and runs comprehensive tests)
|
||||
- **Code formatting**: `./gradlew spotlessApply` (runs automatically before compilation)
|
||||
|
||||
### Docker Development
|
||||
- **Build ultra-lite**: `docker build -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .`
|
||||
- **Build standard**: `docker build -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .`
|
||||
- **Build fat version**: `docker build -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .`
|
||||
- **Example compose files**: Located in `exampleYmlFiles/` directory
|
||||
|
||||
### Security Mode Development
|
||||
Set `DOCKER_ENABLE_SECURITY=true` environment variable to enable security features during development. This is required for testing the full version locally.
|
||||
|
||||
### Frontend Development
|
||||
- **Frontend dev server**: `cd frontend && npm run dev` (requires backend on localhost:8080)
|
||||
- **Tech Stack**: Vite + React + TypeScript + Mantine UI + TailwindCSS
|
||||
- **Proxy Configuration**: Vite proxies `/api/*` calls to backend (localhost:8080)
|
||||
- **Build Process**: DO NOT run build scripts manually - builds are handled by CI/CD pipelines
|
||||
- **Package Installation**: DO NOT run npm install commands - package management handled separately
|
||||
- **Deployment Options**:
|
||||
- **Desktop App**: `npm run tauri-build` (native desktop application)
|
||||
- **Web Server**: `npm run build` then serve dist/ folder
|
||||
- **Development**: `npm run tauri-dev` for desktop dev mode
|
||||
|
||||
#### Multi-Tool Workflow Architecture
|
||||
Frontend designed for **stateful document processing**:
|
||||
- Users upload PDFs once, then chain tools (split → merge → compress → view)
|
||||
- File state and processing results persist across tool switches
|
||||
- No file reloading between tools - performance critical for large PDFs (up to 100GB+)
|
||||
|
||||
#### FileContext - Central State Management
|
||||
**Location**: `src/contexts/FileContext.tsx`
|
||||
- **Active files**: Currently loaded PDFs and their variants
|
||||
- **Tool navigation**: Current mode (viewer/pageEditor/fileEditor/toolName)
|
||||
- **Memory management**: PDF document cleanup, blob URL lifecycle, Web Worker management
|
||||
- **IndexedDB persistence**: File storage with thumbnail caching
|
||||
- **Preview system**: Tools can preview results (e.g., Split → Viewer → back to Split) without context pollution
|
||||
|
||||
**Critical**: All file operations go through FileContext. Don't bypass with direct file handling.
|
||||
|
||||
#### Processing Services
|
||||
- **enhancedPDFProcessingService**: Background PDF parsing and manipulation
|
||||
- **thumbnailGenerationService**: Web Worker-based with main-thread fallback
|
||||
- **fileStorage**: IndexedDB with LRU cache management
|
||||
|
||||
#### Memory Management Strategy
|
||||
**Why manual cleanup exists**: Large PDFs (up to 100GB+) through multiple tools accumulate:
|
||||
- PDF.js documents that need explicit .destroy() calls
|
||||
- Blob URLs from tool outputs that need revocation
|
||||
- Web Workers that need termination
|
||||
Without cleanup: browser crashes with memory leaks.
|
||||
|
||||
#### Tool Development
|
||||
|
||||
**Architecture**: Modular hook-based system with clear separation of concerns:
|
||||
|
||||
- **useToolOperation** (`frontend/src/hooks/tools/shared/useToolOperation.ts`): Main orchestrator hook
|
||||
- Coordinates all tool operations with consistent interface
|
||||
- Integrates with FileContext for operation tracking
|
||||
- Handles validation, error handling, and UI state management
|
||||
|
||||
- **Supporting Hooks**:
|
||||
- **useToolState**: UI state management (loading, progress, error, files)
|
||||
- **useToolApiCalls**: HTTP requests and file processing
|
||||
- **useToolResources**: Blob URLs, thumbnails, ZIP downloads
|
||||
|
||||
- **Utilities**:
|
||||
- **toolErrorHandler**: Standardized error extraction and i18n support
|
||||
- **toolResponseProcessor**: API response handling (single/zip/custom)
|
||||
- **toolOperationTracker**: FileContext integration utilities
|
||||
|
||||
**Three Tool Patterns**:
|
||||
|
||||
**Pattern 1: Single-File Tools** (Individual processing)
|
||||
- Backend processes one file per API call
|
||||
- Set `multiFileEndpoint: false`
|
||||
- Examples: Compress, Rotate
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'compress',
|
||||
endpoint: '/api/v1/misc/compress-pdf',
|
||||
buildFormData: (params, file: File) => { /* single file */ },
|
||||
multiFileEndpoint: false,
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 2: Multi-File Tools** (Batch processing)
|
||||
- Backend accepts `MultipartFile[]` arrays in single API call
|
||||
- Set `multiFileEndpoint: true`
|
||||
- Examples: Split, Merge, Overlay
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'split',
|
||||
endpoint: '/api/v1/general/split-pages',
|
||||
buildFormData: (params, files: File[]) => { /* all files */ },
|
||||
multiFileEndpoint: true,
|
||||
filePrefix: 'split_',
|
||||
});
|
||||
```
|
||||
|
||||
**Pattern 3: Complex Tools** (Custom processing)
|
||||
- Tools with complex routing logic or non-standard processing
|
||||
- Provide `customProcessor` for full control
|
||||
- Examples: Convert, OCR
|
||||
```typescript
|
||||
return useToolOperation({
|
||||
operationType: 'convert',
|
||||
customProcessor: async (params, files) => { /* custom logic */ },
|
||||
});
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- **No Timeouts**: Operations run until completion (supports 100GB+ files)
|
||||
- **Consistent**: All tools follow same pattern and interface
|
||||
- **Maintainable**: Single responsibility hooks, easy to test and modify
|
||||
- **i18n Ready**: Built-in internationalization support
|
||||
- **Type Safe**: Full TypeScript support with generic interfaces
|
||||
- **Memory Safe**: Automatic resource cleanup and blob URL management
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Project Structure
|
||||
- **Backend**: Spring Boot application with Thymeleaf templating
|
||||
- **Frontend**: React-based SPA in `/frontend` directory (Thymeleaf templates fully replaced)
|
||||
- **File Storage**: IndexedDB for client-side file persistence and thumbnails
|
||||
- **Internationalization**: JSON-based translations (converted from backend .properties)
|
||||
- **PDF Processing**: PDFBox for core PDF operations, LibreOffice for conversions, PDF.js for client-side rendering
|
||||
- **Security**: Spring Security with optional authentication (controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- **Configuration**: YAML-based configuration with environment variable overrides
|
||||
|
||||
### Controller Architecture
|
||||
- **API Controllers** (`src/main/java/.../controller/api/`): REST endpoints for PDF operations
|
||||
- Organized by function: converters, security, misc, pipeline
|
||||
- Follow pattern: `@RestController` + `@RequestMapping("/api/v1/...")`
|
||||
- **Web Controllers** (`src/main/java/.../controller/web/`): Serve Thymeleaf templates
|
||||
- Pattern: `@Controller` + return template names
|
||||
|
||||
### Key Components
|
||||
- **SPDFApplication.java**: Main application class with desktop UI and browser launching logic
|
||||
- **ConfigInitializer**: Handles runtime configuration and settings files
|
||||
- **Pipeline System**: Automated PDF processing workflows via `PipelineController`
|
||||
- **Security Layer**: Authentication, authorization, and user management (when enabled)
|
||||
|
||||
### Component Architecture
|
||||
- **React Components**: Located in `frontend/src/components/` and `frontend/src/tools/`
|
||||
- **Static Assets**: CSS, JS, and resources in `src/main/resources/static/` (legacy) + `frontend/public/` (modern)
|
||||
- **Internationalization**:
|
||||
- Backend: `messages_*.properties` files
|
||||
- Frontend: JSON files in `frontend/public/locales/` (converted from .properties)
|
||||
- Conversion Script: `scripts/convert_properties_to_json.py`
|
||||
|
||||
### Configuration Modes
|
||||
- **Ultra-lite**: Basic PDF operations only
|
||||
- **Standard**: Full feature set
|
||||
- **Fat**: Pre-downloaded dependencies for air-gapped environments
|
||||
- **Security Mode**: Adds authentication, user management, and enterprise features
|
||||
|
||||
### Testing Strategy
|
||||
- **Integration Tests**: Cucumber tests in `testing/cucumber/`
|
||||
- **Docker Testing**: `test.sh` validates all Docker variants
|
||||
- **Manual Testing**: No unit tests currently - relies on UI and API testing
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Local Development**:
|
||||
- Backend: `./gradlew bootRun` (runs on localhost:8080)
|
||||
- Frontend: `cd frontend && npm run dev` (runs on localhost:5173, proxies to backend)
|
||||
2. **Docker Testing**: Use `./test.sh` before submitting PRs
|
||||
3. **Code Style**: Spotless enforces Google Java Format automatically
|
||||
4. **Translations**:
|
||||
- Backend: Use helper scripts in `/scripts` for multi-language updates
|
||||
- Frontend: Update JSON files in `frontend/public/locales/` or use conversion script
|
||||
5. **Documentation**: API docs auto-generated and available at `/swagger-ui/index.html`
|
||||
|
||||
## Frontend Architecture Status
|
||||
|
||||
- **Core Status**: React SPA architecture complete with multi-tool workflow support
|
||||
- **State Management**: FileContext handles all file operations and tool navigation
|
||||
- **File Processing**: Production-ready with memory management for large PDF workflows (up to 100GB+)
|
||||
- **Tool Integration**: Modular hook architecture with `useToolOperation` orchestrator
|
||||
- Individual hooks: `useToolState`, `useToolApiCalls`, `useToolResources`
|
||||
- Utilities: `toolErrorHandler`, `toolResponseProcessor`, `toolOperationTracker`
|
||||
- Pattern: Each tool creates focused operation hook, UI consumes state/actions
|
||||
- **Preview System**: Tool results can be previewed without polluting file context (Split tool example)
|
||||
- **Performance**: Web Worker thumbnails, IndexedDB persistence, background processing
|
||||
|
||||
## Translation Rules
|
||||
|
||||
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
|
||||
- Translation files are located in `frontend/public/locales/`
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Minimum JDK 17, supports and recommends JDK 21
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **Desktop Mode**: Set `STIRLING_PDF_DESKTOP_UI=true` for desktop application mode
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
- **Frontend**: Uses IndexedDB for client-side file storage and caching (with thumbnails)
|
||||
- **Security**: When `DOCKER_ENABLE_SECURITY=false`, security-related classes are excluded from compilation
|
||||
- **FileContext**: All file operations MUST go through FileContext - never bypass with direct File handling
|
||||
- **Memory Management**: Manual cleanup required for PDF.js documents and blob URLs - don't remove cleanup code
|
||||
- **Tool Development**: New tools should follow `useToolOperation` hook pattern (see `useCompressOperation.ts`)
|
||||
- **Performance Target**: Must handle PDFs up to 100GB+ without browser crashes
|
||||
- **Preview System**: Tools can preview results without polluting main file context (see Split tool implementation)
|
||||
- **Adding Tools**: See `ADDING_TOOLS.md` for complete guide to creating new PDF tools
|
||||
|
||||
## Communication Style
|
||||
- Be direct and to the point
|
||||
- No apologies or conversational filler
|
||||
- Answer questions directly without preamble
|
||||
- Explain reasoning concisely when asked
|
||||
- Avoid unnecessary elaboration
|
||||
|
||||
## Decision Making
|
||||
- Ask clarifying questions before making assumptions
|
||||
- Stop and ask when uncertain about project-specific details
|
||||
- Confirm approach before making structural changes
|
||||
- Request guidance on preferences (cross-platform vs specific tools, etc.)
|
||||
- Verify understanding of requirements before proceeding
|
||||
@@ -0,0 +1,692 @@
|
||||
# Stirling-PDF Developer Guide
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
Stirling-PDF is a robust, locally hosted, web-based PDF manipulation tool. **Stirling 2.0** represents a complete frontend rewrite, replacing the legacy Thymeleaf-based UI with a modern React SPA (Single Page Application).
|
||||
|
||||
This guide focuses on developing for Stirling 2.0, including both the React frontend and Spring Boot backend development workflows.
|
||||
|
||||
## 2. Project Overview
|
||||
|
||||
**Stirling 2.0** is built using:
|
||||
|
||||
**Backend:**
|
||||
- Spring Boot (Java 17+, JDK 21 recommended)
|
||||
- PDFBox for core PDF operations
|
||||
- LibreOffice for document conversions
|
||||
- qpdf for PDF optimization
|
||||
- Spring Security (optional, controlled by `DOCKER_ENABLE_SECURITY`)
|
||||
- Lombok for reducing boilerplate code
|
||||
|
||||
**Frontend (React SPA):**
|
||||
- React + TypeScript
|
||||
- Vite for build tooling and development server
|
||||
- Mantine UI component library
|
||||
- TailwindCSS for styling
|
||||
- PDF.js for client-side PDF rendering
|
||||
- PDF-LIB.js for client-side PDF manipulation
|
||||
- IndexedDB for client-side file storage and thumbnails
|
||||
- i18next for internationalization
|
||||
|
||||
**Infrastructure:**
|
||||
- Docker for containerization
|
||||
- Gradle for build management
|
||||
|
||||
**Legacy (reference only during development):**
|
||||
- Thymeleaf templates (being completely replaced in 2.0)
|
||||
|
||||
## 3. Development Environment Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 17 or later (JDK 21 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
|
||||
### Setup Steps
|
||||
|
||||
1. Clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
|
||||
cd Stirling-PDF
|
||||
```
|
||||
|
||||
2. Install Docker and JDK17 if not already installed.
|
||||
|
||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||
1. Only VSCode
|
||||
1. Open VS Code.
|
||||
2. When prompted, install the recommended extensions.
|
||||
3. Alternatively, open the command palette (`Ctrl + Shift + P` or `Cmd + Shift + P` on macOS) and run:
|
||||
|
||||
```sh
|
||||
Extensions: Show Recommended Extensions
|
||||
```
|
||||
|
||||
4. Install the required extensions from the list.
|
||||
|
||||
4. Lombok Setup
|
||||
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
|
||||
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
|
||||
|
||||
5. Add environment variable
|
||||
For local testing, you should generally be testing the full 'Security' version of Stirling PDF. To do this, you must add the environment flag DISABLE_ADDITIONAL_FEATURES=false to your system and/or IDE build/run step.
|
||||
5. **Frontend Setup (Required for Stirling 2.0)**
|
||||
Navigate to the frontend directory and install dependencies using npm.
|
||||
|
||||
## 4. Stirling 2.0 Development Workflow
|
||||
|
||||
### Frontend Development (React)
|
||||
The frontend is a React SPA that runs independently during development:
|
||||
|
||||
1. **Start the backend**: Run the Spring Boot application (serves API endpoints on localhost:8080)
|
||||
2. **Start the frontend dev server**: Navigate to the frontend directory and run the development server (serves UI on localhost:5173)
|
||||
3. **Development flow**: The Vite dev server automatically proxies API calls to the backend
|
||||
|
||||
### File Storage Architecture
|
||||
Stirling 2.0 uses client-side file storage:
|
||||
- **IndexedDB**: Stores files locally in the browser with automatic thumbnail generation
|
||||
- **PDF.js**: Handles client-side PDF rendering and processing
|
||||
- **URL Parameters**: Support for deep linking and tool state persistence
|
||||
|
||||
### Legacy Code Reference
|
||||
The existing Thymeleaf templates remain in the codebase during development as reference material but will be completely removed for the 2.0 release.
|
||||
|
||||
## 5. Project Structure
|
||||
|
||||
```bash
|
||||
Stirling-PDF/
|
||||
├── .github/ # GitHub-specific files (workflows, issue templates)
|
||||
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
|
||||
├── frontend/ # React SPA frontend (Stirling 2.0)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── tools/ # Tool-specific React components
|
||||
│ │ ├── hooks/ # Custom React hooks
|
||||
│ │ ├── services/ # API and utility services
|
||||
│ │ ├── types/ # TypeScript type definitions
|
||||
│ │ └── utils/ # Utility functions
|
||||
│ ├── public/
|
||||
│ │ └── locales/ # Internationalization files (JSON)
|
||||
│ ├── package.json # Frontend dependencies
|
||||
│ └── vite.config.ts # Vite configuration
|
||||
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
|
||||
├── docs/ # Documentation files
|
||||
├── exampleYmlFiles/ # Example YAML configuration files
|
||||
├── images/ # Image assets
|
||||
├── pipeline/ # Pipeline-related files (generated at runtime)
|
||||
├── scripts/ # Utility scripts
|
||||
├── src/ # Source code
|
||||
│ ├── main/
|
||||
│ │ ├── java/
|
||||
│ │ │ └── stirling/
|
||||
│ │ │ └── software/
|
||||
│ │ │ └── SPDF/
|
||||
│ │ │ ├── config/
|
||||
│ │ │ ├── controller/
|
||||
│ │ │ ├── model/
|
||||
│ │ │ ├── repository/
|
||||
│ │ │ ├── service/
|
||||
│ │ │ └── utils/
|
||||
│ │ └── resources/
|
||||
│ │ ├── static/ # Legacy static assets (reference only)
|
||||
│ │ │ ├── css/
|
||||
│ │ │ ├── js/
|
||||
│ │ │ └── pdfjs/
|
||||
│ │ └── templates/ # Legacy Thymeleaf templates (reference only)
|
||||
│ └── test/
|
||||
├── testing/ # Cucumber and integration tests
|
||||
│ └── cucumber/ # Cucumber test files
|
||||
├── build.gradle # Gradle build configuration
|
||||
├── Dockerfile # Main Dockerfile
|
||||
├── Dockerfile.ultra-lite # Dockerfile for ultra-lite version
|
||||
├── Dockerfile.fat # Dockerfile for fat version
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
└── test.sh # Test script to deploy all docker versions and run cuke tests
|
||||
```
|
||||
|
||||
## 6. Docker-based Development
|
||||
|
||||
Stirling-PDF offers several Docker versions:
|
||||
|
||||
- Full: All features included
|
||||
- Ultra-Lite: Basic PDF operations only
|
||||
- Fat: Includes additional libraries and fonts predownloaded
|
||||
|
||||
### Example Docker Compose Files
|
||||
|
||||
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory, such as:
|
||||
|
||||
- `docker-compose-latest.yml`: Latest version without login and security features
|
||||
- `docker-compose-latest-security.yml`: Latest version with login and security features enabled
|
||||
- `docker-compose-latest-fat-security.yml`: Fat version with login and security features enabled
|
||||
|
||||
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 4G
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 16
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./stirling/latest/data:/usr/share/tessdata:rw
|
||||
- ./stirling/latest/config:/configs:rw
|
||||
- ./stirling/latest/logs:/logs:rw
|
||||
environment:
|
||||
DISABLE_ADDITIONAL_FEATURES: "false"
|
||||
SECURITY_ENABLELOGIN: "true"
|
||||
PUID: 1002
|
||||
PGID: 1002
|
||||
UMASK: "022"
|
||||
SYSTEM_DEFAULTLOCALE: en-US
|
||||
UI_APPNAME: Stirling-PDF
|
||||
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
|
||||
UI_APPNAMENAVBAR: Stirling-PDF Latest
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "true"
|
||||
SHOW_SURVEY: "true"
|
||||
restart: on-failure:5
|
||||
```
|
||||
|
||||
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
|
||||
|
||||
```bash
|
||||
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
|
||||
```
|
||||
|
||||
### Building Docker Images
|
||||
|
||||
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
|
||||
|
||||
1. Set the security environment variable:
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=true # or false for to enable login and security features for builds
|
||||
```
|
||||
|
||||
2. Build the project with Gradle:
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
3. Build the Docker images:
|
||||
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
```
|
||||
|
||||
For the ultra-lite version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with login and security features enabled):
|
||||
|
||||
```bash
|
||||
export DISABLE_ADDITIONAL_FEATURES=false
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
|
||||
## 7. Testing
|
||||
|
||||
### Comprehensive Testing Script
|
||||
|
||||
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
|
||||
|
||||
To run the test script:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
This script performs the following actions:
|
||||
|
||||
1. Builds all Docker images (full, ultra-lite, fat).
|
||||
2. Runs each version to ensure it starts correctly.
|
||||
3. Executes Cucumber tests against the main version and ensures feature compatibility. In the event these tests fail, your PR will not be merged.
|
||||
|
||||
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
|
||||
|
||||
### Full Testing with Docker
|
||||
|
||||
1. Build and run the Docker container per the above instructions:
|
||||
|
||||
2. Access the application at `http://localhost:8080` and manually test all features developed.
|
||||
|
||||
### Frontend Development Testing (Stirling 2.0)
|
||||
|
||||
For React frontend development:
|
||||
|
||||
1. Start the backend: Run the Spring Boot application to serve API endpoints on localhost:8080
|
||||
2. Start the frontend dev server: Navigate to the frontend directory and run the development server on localhost:5173
|
||||
3. The Vite dev server automatically proxies API calls to the backend
|
||||
4. Test React components, UI interactions, and IndexedDB file operations using browser developer tools
|
||||
|
||||
### Local Testing (Java and UI Components)
|
||||
|
||||
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
|
||||
|
||||
- Java backend logic
|
||||
- RESTful API endpoints
|
||||
- JavaScript functionality
|
||||
- User interface components and styling
|
||||
- Thymeleaf templates
|
||||
|
||||
To run Stirling-PDF locally:
|
||||
|
||||
1. Compile and run the project using built-in IDE methods or by running:
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
2. Access the application at `http://localhost:8080` in your web browser.
|
||||
|
||||
3. Manually test the features you're working on through the UI.
|
||||
|
||||
4. For API changes, use tools like Postman or curl to test endpoints directly.
|
||||
|
||||
Important notes:
|
||||
|
||||
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
## 8. Contributing
|
||||
|
||||
1. Fork the repository on GitHub.
|
||||
2. Create a new branch for your feature or bug fix.
|
||||
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
|
||||
4. Test your changes thoroughly in the Docker environment.
|
||||
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
|
||||
|
||||
```bash
|
||||
./test.sh
|
||||
```
|
||||
|
||||
6. Push your changes to your fork.
|
||||
7. Submit a pull request to the main repository.
|
||||
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md).
|
||||
|
||||
When you raise a PR:
|
||||
|
||||
- The `test.sh` script will run automatically against your PR.
|
||||
- The PR checks will verify versioning and dependency updates.
|
||||
- Documentation will be automatically updated for dependency changes.
|
||||
- Security issues will be checked using Snyk and PixeeBot.
|
||||
|
||||
Address any issues that arise from these checks before finalizing your pull request.
|
||||
|
||||
## 9. API Documentation
|
||||
|
||||
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
|
||||
|
||||
## 10. Customization
|
||||
|
||||
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
|
||||
|
||||
- Application name and branding
|
||||
- Security settings
|
||||
- UI customization
|
||||
- Endpoint management
|
||||
|
||||
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
|
||||
```
|
||||
|
||||
Refer to the main README for a full list of customization options.
|
||||
|
||||
## 11. Language Translations
|
||||
|
||||
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
|
||||
|
||||
```bash
|
||||
/scripts/replace_translation_line.sh
|
||||
```
|
||||
|
||||
This script helps you make consistent replacements across language files.
|
||||
|
||||
When contributing translations:
|
||||
|
||||
1. Use the helper script for multi-file changes.
|
||||
2. Ensure all language files are updated consistently.
|
||||
3. The PR checks will verify consistency in language file updates.
|
||||
|
||||
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
|
||||
|
||||
## Code examples
|
||||
|
||||
### React Component Development (Stirling 2.0)
|
||||
|
||||
For Stirling 2.0, new features are built as React components instead of Thymeleaf templates:
|
||||
|
||||
#### Creating a New Tool Component
|
||||
|
||||
1. **Create the React Component:**
|
||||
```typescript
|
||||
// frontend/src/tools/NewTool.tsx
|
||||
import { useState } from 'react';
|
||||
import { Button, FileInput, Container } from '@mantine/core';
|
||||
|
||||
interface NewToolProps {
|
||||
params: Record<string, any>;
|
||||
updateParams: (updates: Record<string, any>) => void;
|
||||
}
|
||||
|
||||
export default function NewTool({ params, updateParams }: NewToolProps) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
||||
const handleProcess = async () => {
|
||||
// Process files using API or client-side logic
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<FileInput
|
||||
multiple
|
||||
accept="application/pdf"
|
||||
onChange={setFiles}
|
||||
/>
|
||||
<Button onClick={handleProcess}>Process</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add API Integration:**
|
||||
```typescript
|
||||
// Use existing API endpoints or create new ones
|
||||
const response = await fetch('/api/v1/new-tool', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
```
|
||||
|
||||
3. **Register in Tool Picker:**
|
||||
Update the tool picker component to include the new tool with proper routing and URL parameter support.
|
||||
|
||||
### Legacy Reference: Overview of Thymeleaf
|
||||
|
||||
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot.
|
||||
|
||||
### Thymeleaf overview
|
||||
|
||||
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `stirling-pdf/src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
|
||||
|
||||
Some examples of this are:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
```
|
||||
or
|
||||
```html
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
```
|
||||
|
||||
Where it uses the `th:block`, `th:` indicating it's a special Thymeleaf element to be used server-side in generating the HTML, and block being the actual element type.
|
||||
In this case, we are inserting the `navbar` entry within the `fragments/navbar.html` fragment into the `th:block` element.
|
||||
|
||||
They can be more complex, such as:
|
||||
|
||||
```html
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
|
||||
```
|
||||
|
||||
Which is the same as above but passes the parameters title and header into the fragment `common.html` to be used in its HTML generation.
|
||||
|
||||
Thymeleaf can also be used to loop through objects or pass things from the Java side into the HTML side.
|
||||
|
||||
```java
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("exampleData", exampleData);
|
||||
return "new-feature";
|
||||
}
|
||||
```
|
||||
|
||||
In the above example, if exampleData is a list of plain java objects of class Person and within it, you had id, name, age, etc. You can reference it like so
|
||||
|
||||
```html
|
||||
<tbody>
|
||||
<!-- Use th:each to iterate over the list -->
|
||||
<tr th:each="person : ${exampleData}">
|
||||
<td th:text="${person.id}"></td>
|
||||
<td th:text="${person.name}"></td>
|
||||
<td th:text="${person.age}"></td>
|
||||
<td th:text="${person.email}"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
```
|
||||
|
||||
This would generate n entries of tr for each person in exampleData
|
||||
|
||||
### Adding a New Feature to the Backend (API)
|
||||
|
||||
1. **Create a New Controller:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/api` directory.
|
||||
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
|
||||
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Define the Service Layer:** (Not required but often useful)
|
||||
- Create a new service class in the `stirling-pdf/src/main/java/stirling/software/SPDF/service` directory.
|
||||
- Implement the business logic for the new feature.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class NewFeatureService {
|
||||
|
||||
public String getNewFeatureData() {
|
||||
// Implement business logic here
|
||||
return "New Feature Data";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2b. **Integrate the Service with the Controller:**
|
||||
|
||||
- Autowire the service class in the controller and use it to handle the API request.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/new-feature")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
public class NewFeatureController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
|
||||
public String newFeature() {
|
||||
return newFeatureService.getNewFeatureData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding a New Feature to the Frontend (UI)
|
||||
|
||||
1. **Create a New Thymeleaf Template:**
|
||||
- Create a new HTML file in the `stirling-pdf/src/main/resources/templates` directory.
|
||||
- Use Thymeleaf attributes to dynamically generate content.
|
||||
- Use `extract-page.html` as a base example for the HTML template, which is useful to ensure importing of the general layout, navbar, and footer.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
|
||||
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
|
||||
</div>
|
||||
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
|
||||
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
|
||||
<input type="hidden" id="customMode" name="customMode" value="">
|
||||
<div class="mb-3">
|
||||
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
|
||||
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
2. **Create a New Controller for the UI:**
|
||||
- Create a new Java class in the `stirling-pdf/src/main/java/stirling/software/SPDF/controller/ui` directory.
|
||||
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
|
||||
|
||||
```java
|
||||
package stirling.software.SPDF.controller.ui;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import stirling.software.SPDF.service.NewFeatureService;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/new-feature")
|
||||
public class NewFeatureUIController {
|
||||
|
||||
@Autowired
|
||||
private NewFeatureService newFeatureService;
|
||||
|
||||
@GetMapping
|
||||
public String newFeaturePage(Model model) {
|
||||
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
|
||||
return "new-feature";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update the Navigation Bar:**
|
||||
- Add a link to the new feature page in the navigation bar.
|
||||
- Update the `stirling-pdf/src/main/resources/templates/fragments/navbar.html` file.
|
||||
|
||||
```html
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" th:href="@{'/new-feature'}">New Feature</a>
|
||||
</li>
|
||||
```
|
||||
|
||||
## Adding New Translations to Existing Language Files in Stirling-PDF
|
||||
|
||||
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
|
||||
|
||||
### 1. Locate Existing Language Files
|
||||
|
||||
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||
|
||||
- `messages.properties` (default, usually English)
|
||||
- `messages_en_GB.properties`
|
||||
- `messages_fr_FR.properties`
|
||||
- `messages_de_DE.properties`
|
||||
- etc.
|
||||
|
||||
### 2. Add New Translation Entries
|
||||
|
||||
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
|
||||
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
|
||||
you might add:
|
||||
|
||||
```properties
|
||||
pdfSplitter.title=PDF Splitter
|
||||
pdfSplitter.description=Split your PDF into multiple documents
|
||||
pdfSplitter.button.split=Split PDF
|
||||
pdfSplitter.input.pages=Enter page numbers to split
|
||||
```
|
||||
|
||||
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
|
||||
|
||||
### 3. Use Translations in Thymeleaf Templates
|
||||
|
||||
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
|
||||
|
||||
```html
|
||||
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
|
||||
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
|
||||
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
|
||||
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
|
||||
```
|
||||
|
||||
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.
|
||||
@@ -1,60 +0,0 @@
|
||||
# dockerfile.dev
|
||||
|
||||
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
|
||||
FROM gradle:8.14-jdk17
|
||||
|
||||
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
|
||||
USER root
|
||||
|
||||
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
|
||||
ENV GRADLE_HOME=/opt/gradle
|
||||
ENV PATH="$GRADLE_HOME/bin:$PATH"
|
||||
|
||||
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
|
||||
RUN apt-get update && apt-get install -y \
|
||||
sudo \
|
||||
libreoffice \
|
||||
poppler-utils \
|
||||
qpdf \
|
||||
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
|
||||
tesseract-ocr \
|
||||
tesseract-ocr-eng \
|
||||
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine fonts-urw-base35 \
|
||||
python3-uno \
|
||||
python3-venv \
|
||||
# ss -tln
|
||||
iproute2 \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Setze die Environment Variable für setuptools
|
||||
ENV SETUPTOOLS_USE_DISTUTILS=local \
|
||||
STIRLING_TEMPFILES_DIRECTORY=/tmp/stirling-pdf \
|
||||
TMPDIR=/tmp/stirling-pdf \
|
||||
TEMP=/tmp/stirling-pdf \
|
||||
TMP=/tmp/stirling-pdf
|
||||
|
||||
# Installation der benötigten Python-Pakete
|
||||
COPY .github/scripts/requirements_dev.txt /tmp/requirements_dev.txt
|
||||
RUN python3 -m venv --system-site-packages /opt/venv \
|
||||
&& . /opt/venv/bin/activate \
|
||||
&& pip install --no-cache-dir --require-hashes -r /tmp/requirements_dev.txt
|
||||
|
||||
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
|
||||
ENV PATH="/opt/venv/bin:$PATH"
|
||||
|
||||
COPY . /workspace
|
||||
|
||||
RUN mkdir -p /tmp/stirling-pdf \
|
||||
&& fc-cache -f -v \
|
||||
&& adduser --disabled-password --gecos '' devuser \
|
||||
&& chown -R devuser:devuser /home/devuser /workspace /tmp/stirling-pdf
|
||||
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
|
||||
&& chmod 0440 /etc/sudoers.d/devuser
|
||||
|
||||
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
|
||||
WORKDIR /workspace
|
||||
|
||||
RUN chmod +x /workspace/.devcontainer/git-init.sh /workspace/.devcontainer/init-setup.sh
|
||||
|
||||
# Wechsel zum Nicht‑Root Benutzer
|
||||
USER devuser
|
||||
@@ -21,97 +21,81 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
|
||||
## Features
|
||||
|
||||
- 50+ PDF Operations
|
||||
- Parallel file processing and downloads
|
||||
- Dark mode support
|
||||
- Custom download options
|
||||
- Custom 'Pipelines' to run multiple features in an automated queue
|
||||
- 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)
|
||||
- Enterprise features like SSO (see [here](https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration) 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)
|
||||
|
||||
### 50+ PDF Operations
|
||||
## PDF Features
|
||||
|
||||
#### Organise
|
||||
- **Merge**: Combine multiple PDFs into one
|
||||
- **Split**: Divide PDFs into multiple files
|
||||
- **Extract page(s)**: Extract specific pages from PDF
|
||||
- **Remove**: Delete pages from PDF
|
||||
- **Crop PDF**: Adjust PDF page boundaries
|
||||
- **Rotate**: Rotate pages in 90-degree increments
|
||||
- **Adjust page size/scale**: Resize page contents
|
||||
- **Multi-Page Layout**: Add multiple pages to PDF
|
||||
- **PDF to Single Large Page**: Convert to single continuous page
|
||||
- **Organize**: Rearrange PDF pages
|
||||
### Page Operations
|
||||
|
||||
#### Convert to PDF
|
||||
- **Image to PDF**: Convert images to PDF format
|
||||
- **Convert file to PDF**: Convert various common file types to PDF
|
||||
- **HTML to PDF**: Transform HTML documents to PDF
|
||||
- **Markdown to PDF**: Convert Markdown files to PDF
|
||||
- **CBZ to PDF**: Convert comic book archives
|
||||
- **CBR to PDF**: Convert comic book rar archives
|
||||
- **Email to PDF**: Convert email files to PDF
|
||||
- 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
|
||||
|
||||
#### Convert from PDF
|
||||
- **PDF to Word**: Convert to documet (docx, doc, odt) format
|
||||
- **PDF to Image**: Extract PDF pages as images
|
||||
- **PDF to RTF (Text)**: Convert to Rich Text Format
|
||||
- **PDF to Presentation**: Convert to presentation (pptx, ppt, odp) format
|
||||
- **PDF to CSV**: Extract tables to CSV
|
||||
- **PDF to XML**: Convert to XML format
|
||||
- **PDF to HTML**: Transform to HTML
|
||||
- **PDF to PDF/A**: Convert to archival (PDF/A-1b, PDF/A-2b) format
|
||||
- **PDF to Markdown**: Convert PDF to Markdown
|
||||
- **PDF to CBZ**: Convert to comic book archive
|
||||
- **PDF to CBR**: Convert to comic book rar archive
|
||||
### Conversion Operations
|
||||
|
||||
#### Sign & Security
|
||||
- **Sign**: Add digital signatures
|
||||
- **Remove Password**: Remove PDF security
|
||||
- **Add Watermark**: Apply watermarks
|
||||
- **Sign with Certificate**: Certificate-based signing
|
||||
- **Add Stamp to PDF**: Apply digital stamps
|
||||
- **Auto Redact**: Automatically redact content
|
||||
- **Change Permissions**: Modify access permissions
|
||||
- **Add Password**: Apply PDF encryption
|
||||
- **Manual Redaction**: Manual content redaction
|
||||
- **Remove Certificate Sign**: Remove digital signatures
|
||||
- **Sanitize**: Clean PDF of potential security issues
|
||||
- **Validate PDF Signature**: Verify digital signatures
|
||||
- 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
|
||||
|
||||
### Security & Permissions
|
||||
|
||||
- Add and remove passwords
|
||||
- Change/set PDF permissions
|
||||
- Add watermark(s)
|
||||
- Certify/sign PDFs
|
||||
- Sanitize PDFs
|
||||
- Auto-redact text
|
||||
|
||||
### Other Operations
|
||||
|
||||
- Add/generate/write signatures
|
||||
- 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
|
||||
|
||||
#### View & Edit
|
||||
- **OCR / Cleanup scans**: Optical Character Recognition
|
||||
- **Add Image**: Insert images into PDF
|
||||
- **Extract Images**: Extract embedded images
|
||||
- **Change Metadata**: Edit PDF metadata
|
||||
- **Get ALL Info on PDF**: Comprehensive PDF analysis
|
||||
- **Advanced Colour options**: Colour manipulation (various options for colour inversion, CMYK conversion)
|
||||
- **Compare**: Compare PDF documents
|
||||
- **Add Page Numbers**: Insert page numbering
|
||||
- **Flatten**: Flatten PDF layers, and interactive elements
|
||||
- **Remove Annotations**: Delete comments and markups
|
||||
- **Remove Blank pages**: Delete empty pages
|
||||
- **Remove Image**: Delete embedded images
|
||||
- **View/Edit PDF**: Interactive PDF editing
|
||||
- **Unlock PDF Forms**: Enable form editing
|
||||
- **Add Attachments**: Attach files to PDF
|
||||
|
||||
#### Advanced
|
||||
- **Compress**: Reduce file size
|
||||
- **Pipeline**: Automated workflow processing (OCR images pipeline, prepare PDFs for emailing pipeline)
|
||||
- **Adjust Colours/Contrast**: Colour and contrast adjustment
|
||||
- **Auto Rename PDF File**: Automatic file renaming
|
||||
- **Auto Split Pages**: Automatic page splitting
|
||||
- **Detect/Split Scanned photos**: Photo detection and splitting
|
||||
- **Overlay PDFs**: Layer PDFs over each other
|
||||
- **Repair**: Fix corrupted PDFs
|
||||
- **Show JavaScript**: Display embedded JavaScript
|
||||
- **Auto Split by Size/Count**: Split by file size or page count
|
||||
- **Split PDF by Chapters**: Chapter-based splitting
|
||||
- **Split PDF by Sections**: Section-based splitting
|
||||
- **Scanner Effect**: Apply scanner-like effects
|
||||
- **Edit Table of Contents**: Modify PDF bookmarks and TOC
|
||||
|
||||
# 📖 Get Started
|
||||
|
||||
@@ -130,48 +114,48 @@ Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.sti
|
||||
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) |  |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| 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) |  |
|
||||
| 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) |  |
|
||||
| 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) |  |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ spotless {
|
||||
dependencies {
|
||||
api 'org.springframework.boot:spring-boot-starter-web'
|
||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
// api 'org.springframework.boot:spring-boot-starter-thymeleaf' // Deprecated - UI moved to React frontend
|
||||
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'
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
|
||||
/**
|
||||
* Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework.
|
||||
*
|
||||
@@ -30,6 +32,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
@RequestBody(required = true)
|
||||
public @interface AutoJobPostMapping {
|
||||
|
||||
/** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Account Security API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/account"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/account")
|
||||
@Tag(
|
||||
name = "Account Security",
|
||||
description =
|
||||
"""
|
||||
Account security and protection services for user safety and organizational compliance.
|
||||
|
||||
This endpoint group provides account security capabilities for organizations requiring
|
||||
enhanced protection against unauthorized access, security threats, and compliance violations.
|
||||
|
||||
Common use cases:
|
||||
• Corporate security policy compliance and SOX, HIPAA, GDPR requirements
|
||||
• Fraud prevention, identity theft protection, and account compromise recovery
|
||||
• Multi-factor authentication implementation and insider threat mitigation
|
||||
• Account recovery and emergency access procedures
|
||||
|
||||
Business applications:
|
||||
• Enterprise risk management, security governance, and customer trust protection
|
||||
• Legal liability reduction and insurance requirement fulfillment
|
||||
• Audit preparation, compliance reporting, and business continuity management
|
||||
|
||||
Operational scenarios:
|
||||
• Security incident response, forensic investigation, and user training
|
||||
• Emergency account lockdown, suspicious activity monitoring, and compliance documentation
|
||||
|
||||
Target users: Security administrators, compliance officers, and organizations
|
||||
prioritizing account security and regulatory compliance.
|
||||
""")
|
||||
public @interface AccountSecurityApi {}
|
||||
@@ -0,0 +1,48 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Admin Settings API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/settings"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/settings")
|
||||
@Tag(
|
||||
name = "Admin Settings",
|
||||
description =
|
||||
"""
|
||||
System administration and configuration management for enterprise deployments.
|
||||
|
||||
This endpoint group provides administrative control for organizations deploying
|
||||
Stirling PDF in production environments with multi-user scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Enterprise deployment configuration and multi-tenant environment management
|
||||
• Security policy enforcement, compliance monitoring, and capacity planning
|
||||
• Operational maintenance, troubleshooting, and enterprise infrastructure integration
|
||||
• Disaster recovery and business continuity preparation
|
||||
|
||||
Business applications:
|
||||
• Corporate IT governance, policy enforcement, and compliance reporting
|
||||
• Cost optimization, SLA monitoring, and vendor management oversight
|
||||
• Risk management and security incident response
|
||||
|
||||
Operational scenarios:
|
||||
• 24/7 production monitoring, scheduled maintenance, and system updates
|
||||
• Emergency response, change management, and performance optimization
|
||||
|
||||
Target users: IT administrators, system engineers, and operations teams
|
||||
responsible for enterprise-grade document processing infrastructure.
|
||||
""")
|
||||
public @interface AdminApi {}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Admin Server Certificate API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/server-certificate"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/server-certificate")
|
||||
@Tag(
|
||||
name = "Admin - Server Certificate",
|
||||
description =
|
||||
"""
|
||||
Server certificate management for secure enterprise deployments and encrypted communications.
|
||||
|
||||
This endpoint group provides certificate lifecycle management for organizations
|
||||
requiring secure communications in document processing infrastructure.
|
||||
|
||||
Common use cases:
|
||||
• Corporate security compliance and encrypted communications for healthcare/finance
|
||||
• Customer data protection, internal audits, and multi-environment standardization
|
||||
• Third-party security assessments and disaster recovery security measures
|
||||
|
||||
Business applications:
|
||||
• Enterprise security governance, client trust protection, and secure B2B exchange
|
||||
• Legal requirement fulfillment, liability reduction, and M&A security preparation
|
||||
|
||||
Operational scenarios:
|
||||
• Certificate renewal, emergency replacement, and security incident response
|
||||
• Multi-site deployment coordination and cloud migration preparation
|
||||
|
||||
Target users: Security administrators, compliance officers, and IT infrastructure
|
||||
teams requiring enterprise-grade security for document processing systems.
|
||||
""")
|
||||
public @interface AdminServerCertificateApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Analysis API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/analysis"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/analysis")
|
||||
@Tag(
|
||||
name = "Analysis",
|
||||
description =
|
||||
"""
|
||||
Document analysis and information extraction services for content intelligence and insights.
|
||||
|
||||
This endpoint group provides analytical capabilities to understand document structure,
|
||||
extract information, and generate insights from PDF content for automated processing.
|
||||
|
||||
Common use cases:
|
||||
• Document inventory management and content audit for compliance verification
|
||||
• Quality assurance workflows and business intelligence analytics
|
||||
• Migration planning, accessibility evaluation, and document forensics
|
||||
|
||||
Business applications:
|
||||
• Legal discovery, financial document review, and healthcare records analysis
|
||||
• Academic research, government processing, and publishing optimization
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale profiling, migration assessment, and performance optimization
|
||||
• Automated quality control and content strategy development
|
||||
|
||||
Target users: Data analysts, QA teams, administrators, and business intelligence
|
||||
professionals requiring detailed document insights.
|
||||
""")
|
||||
public @interface AnalysisApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Configuration API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/config"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/config")
|
||||
@Tag(
|
||||
name = "Config",
|
||||
description =
|
||||
"""
|
||||
System configuration management for deployment optimization and operational control.
|
||||
|
||||
This endpoint group provides system configuration capabilities for organizations
|
||||
deploying and operating Stirling PDF in various environments.
|
||||
|
||||
Common use cases:
|
||||
• Environment-specific deployment and performance tuning for varying workloads
|
||||
• Resource optimization, cost management, and infrastructure integration
|
||||
• Compliance configuration, disaster recovery, and multi-environment standardization
|
||||
|
||||
Business applications:
|
||||
• Operational cost optimization, SLA compliance, and risk management
|
||||
• Vendor integration, change management, and capacity planning
|
||||
|
||||
Operational scenarios:
|
||||
• System deployment, performance troubleshooting, and emergency changes
|
||||
• Planned maintenance and multi-site deployment coordination
|
||||
|
||||
Target users: System administrators, DevOps engineers, and IT operations teams
|
||||
responsible for deployment configuration and system optimization.
|
||||
""")
|
||||
public @interface ConfigApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Convert API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/convert"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(
|
||||
name = "Convert",
|
||||
description =
|
||||
"""
|
||||
Document format transformation services for cross-platform compatibility and workflow integration.
|
||||
|
||||
This endpoint group enables transformation between various formats, supporting
|
||||
diverse business workflows and system integrations for mixed document ecosystems.
|
||||
|
||||
Common use cases:
|
||||
• Legacy system integration, document migration, and cross-platform sharing
|
||||
• Archive standardization, publishing preparation, and content adaptation
|
||||
• Accessibility compliance and mobile-friendly document preparation
|
||||
|
||||
Business applications:
|
||||
• Enterprise content management, digital publishing, and educational platforms
|
||||
• Legal document processing, healthcare interoperability, and government standardization
|
||||
|
||||
Integration scenarios:
|
||||
• API-driven pipelines, automated workflow preparation, and batch conversions
|
||||
• Real-time format adaptation for user requests
|
||||
|
||||
Target users: System integrators, content managers, digital archivists, and
|
||||
organizations requiring flexible document format interoperability.
|
||||
""")
|
||||
public @interface ConvertApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Database Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/database"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/database")
|
||||
@Tag(
|
||||
name = "Database",
|
||||
description =
|
||||
"""
|
||||
Database operations for data protection and business continuity management.
|
||||
|
||||
This endpoint group provides essential database operations for organizations requiring
|
||||
reliable data protection and recovery capabilities.
|
||||
|
||||
Common use cases:
|
||||
• Regular data backup, disaster recovery, and business continuity planning
|
||||
• System migration, compliance management, and development environment support
|
||||
• Operational troubleshooting and scheduled maintenance operations
|
||||
|
||||
Business applications:
|
||||
• Risk management, regulatory compliance, and operational resilience
|
||||
• Data governance, change management, and quality assurance support
|
||||
|
||||
Operational scenarios:
|
||||
• Routine backup, emergency recovery, and system maintenance preparation
|
||||
• Data migration projects and performance monitoring
|
||||
|
||||
Target users: Operations teams, system administrators, and organizations requiring
|
||||
reliable data protection and operational database management.
|
||||
""")
|
||||
public @interface DatabaseApi {}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Database Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/admin/database"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/admin/database")
|
||||
@Tag(
|
||||
name = "Database Management",
|
||||
description =
|
||||
"""
|
||||
Enterprise database administration for production data management and business continuity.
|
||||
|
||||
This endpoint group provides database administration capabilities for organizations
|
||||
operating Stirling PDF in production environments.
|
||||
|
||||
Common use cases:
|
||||
• Business continuity, disaster recovery, and regulatory compliance requirements
|
||||
• Performance optimization, data security, and system migration projects
|
||||
• Audit preparation, compliance reporting, and cost optimization
|
||||
|
||||
Business applications:
|
||||
• Enterprise risk management, regulatory compliance, and SLA monitoring
|
||||
• Data retention policies, security incident response, and vendor oversight
|
||||
|
||||
Operational scenarios:
|
||||
• Scheduled maintenance, emergency recovery, and capacity planning
|
||||
• Performance troubleshooting and multi-environment deployment coordination
|
||||
|
||||
Target users: Database administrators, IT operations teams, and enterprise
|
||||
administrators responsible for production data management and system reliability.
|
||||
""")
|
||||
public @interface DatabaseManagementApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Filter API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/filter"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/filter")
|
||||
@Tag(
|
||||
name = "Filter",
|
||||
description =
|
||||
"""
|
||||
Document content filtering and search operations for information discovery and organization.
|
||||
|
||||
This endpoint group enables intelligent content discovery and organization within
|
||||
document collections for content-based processing and information extraction.
|
||||
|
||||
Common use cases:
|
||||
• Legal discovery, research organization, and compliance auditing
|
||||
• Content moderation, academic research, and business intelligence
|
||||
• Quality assurance and content validation workflows
|
||||
|
||||
Business applications:
|
||||
• Contract analysis, financial review, and healthcare records organization
|
||||
• Government processing, educational curation, and IP protection
|
||||
|
||||
Workflow scenarios:
|
||||
• Large-scale processing, automated classification, and information extraction
|
||||
• Document preparation for further processing or analysis
|
||||
|
||||
Target users: Legal professionals, researchers, compliance officers, and
|
||||
organizations requiring intelligent document content discovery and organization.
|
||||
""")
|
||||
public @interface FilterApi {}
|
||||
@@ -0,0 +1,42 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for General PDF processing API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/general"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(
|
||||
name = "General",
|
||||
description =
|
||||
"""
|
||||
Core PDF processing operations for fundamental document manipulation workflows.
|
||||
|
||||
This endpoint group provides essential PDF functionality that forms the foundation
|
||||
of most document processing workflows across various industries.
|
||||
|
||||
Common use cases:
|
||||
• Document preparation for archival systems and content organization
|
||||
• File preparation for distribution, accessibility compliance, and batch processing
|
||||
• Document consolidation for reporting and legal compliance workflows
|
||||
|
||||
Typical applications:
|
||||
• Content management, publishing workflows, and educational content distribution
|
||||
• Business process automation and archive management
|
||||
|
||||
Target users: Content managers, document processors, and organizations requiring
|
||||
reliable foundational PDF manipulation capabilities.
|
||||
""")
|
||||
public @interface GeneralApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Info API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/info"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/info")
|
||||
@Tag(
|
||||
name = "Info",
|
||||
description =
|
||||
"""
|
||||
System information and operational insights for monitoring and performance management.
|
||||
|
||||
This endpoint group provides system information and operational metrics for organizations
|
||||
operating Stirling PDF in production environments.
|
||||
|
||||
Common use cases:
|
||||
• System health monitoring, performance optimization, and capacity planning
|
||||
• Troubleshooting, compliance monitoring, and SLA reporting
|
||||
• Cost optimization, usage analysis, and security monitoring
|
||||
|
||||
Business applications:
|
||||
• Operational cost management, business continuity monitoring, and vendor management
|
||||
• Compliance reporting, strategic planning, and customer service tracking
|
||||
|
||||
Operational scenarios:
|
||||
• 24/7 monitoring, scheduled maintenance, and emergency response coordination
|
||||
• System upgrade planning and capacity scaling decisions
|
||||
|
||||
Target users: Operations teams, system administrators, and management teams requiring
|
||||
operational insights and system performance visibility.
|
||||
""")
|
||||
public @interface InfoApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Miscellaneous API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/misc"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(
|
||||
name = "Misc",
|
||||
description =
|
||||
"""
|
||||
Specialized utilities and supplementary tools for enhanced document processing workflows.
|
||||
|
||||
This endpoint group provides utility operations that support core document processing
|
||||
tasks and address specific workflow needs in real-world scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Document optimization for bandwidth-limited environments and storage cost management
|
||||
• Document repair, content extraction, and validation for quality assurance
|
||||
• Accessibility improvement and custom processing for specialized needs
|
||||
|
||||
Business applications:
|
||||
• Web publishing optimization, email attachment management, and archive efficiency
|
||||
• Mobile compatibility, print production, and legacy document recovery
|
||||
|
||||
Operational scenarios:
|
||||
• Batch processing, quality control, and performance optimization
|
||||
• Troubleshooting and recovery of problematic documents
|
||||
|
||||
Target users: System administrators, document specialists, and organizations requiring
|
||||
specialized document processing and optimization tools.
|
||||
""")
|
||||
public @interface MiscApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Pipeline API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/pipeline"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/pipeline")
|
||||
@Tag(
|
||||
name = "Pipeline",
|
||||
description =
|
||||
"""
|
||||
Automated document processing workflows for complex multi-stage business operations.
|
||||
|
||||
This endpoint group enables organizations to create sophisticated document processing
|
||||
workflows that combine multiple operations into streamlined, repeatable processes.
|
||||
|
||||
Common use cases:
|
||||
• Invoice processing, legal document review, and healthcare records standardization
|
||||
• Government processing, educational content preparation, and publishing automation
|
||||
• Contract lifecycle management and approval processes
|
||||
|
||||
Business applications:
|
||||
• Automated compliance reporting, large-scale migration, and quality assurance
|
||||
• Archive preparation, content delivery, and document approval workflows
|
||||
|
||||
Operational scenarios:
|
||||
• Scheduled batch processing and event-driven document processing
|
||||
• Multi-department coordination and business system integration
|
||||
|
||||
Target users: Business process managers, IT automation specialists, and organizations
|
||||
requiring consistent, repeatable document processing workflows.
|
||||
""")
|
||||
public @interface PipelineApi {}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Proprietary UI Data API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/proprietary/ui-data"), and OpenAPI @Tag. Note:
|
||||
* Controllers using this annotation should also add @EnterpriseEndpoint.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/proprietary/ui-data")
|
||||
@Tag(
|
||||
name = "Proprietary UI Data",
|
||||
description =
|
||||
"""
|
||||
Enterprise user interface data services for commercial deployments and advanced business features.
|
||||
|
||||
This endpoint group provides enhanced data services for commercial and enterprise features,
|
||||
supporting advanced business workflows and professional-grade functionality.
|
||||
|
||||
Common use cases:
|
||||
• Enterprise-grade dashboards, multi-tenant deployment, and business intelligence
|
||||
• Organizational hierarchy management and commercial feature licensing
|
||||
• Professional support integration and advanced workflow automation
|
||||
|
||||
Business applications:
|
||||
• ERP integration, CRM development, and executive reporting dashboards
|
||||
• Multi-subsidiary management, professional service delivery, and compliance interfaces
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale deployment management and white-label solution development
|
||||
• Advanced system integration and commercial feature rollout
|
||||
|
||||
Target users: Enterprise administrators, business analysts, and organizations utilizing
|
||||
commercial features and advanced business capabilities.
|
||||
""")
|
||||
public @interface ProprietaryUiDataApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Security API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/security"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(
|
||||
name = "Security",
|
||||
description =
|
||||
"""
|
||||
Document security and protection services for confidential and sensitive content.
|
||||
|
||||
This endpoint group provides essential security operations for organizations handling
|
||||
sensitive documents and materials requiring controlled access.
|
||||
|
||||
Common use cases:
|
||||
• Legal confidentiality, healthcare privacy (HIPAA), and financial regulatory compliance
|
||||
• Government classified handling, corporate IP protection, and educational privacy (FERPA)
|
||||
• Contract security for business transactions
|
||||
|
||||
Business applications:
|
||||
• Document authentication, confidential sharing, and secure archiving
|
||||
• Content watermarking, access control, and privacy protection through redaction
|
||||
|
||||
Industry scenarios:
|
||||
• Legal discovery, medical records exchange, financial audit documentation
|
||||
• Enterprise policy enforcement and data governance
|
||||
|
||||
Target users: Legal professionals, healthcare administrators, compliance officers,
|
||||
government agencies, and enterprises handling sensitive content.
|
||||
""")
|
||||
public @interface SecurityApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Settings API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/settings"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/settings")
|
||||
@Tag(
|
||||
name = "Settings",
|
||||
description =
|
||||
"""
|
||||
User preferences and application customization for personalized workflow optimization.
|
||||
|
||||
This endpoint group provides preference management capabilities for users and
|
||||
organizations to customize their document processing experience.
|
||||
|
||||
Common use cases:
|
||||
• Workflow optimization, accessibility compliance, and corporate branding
|
||||
• Multi-language support, user personalization, and business system integration
|
||||
• Organizational policy compliance
|
||||
|
||||
Business applications:
|
||||
• Corporate branding, productivity optimization, and accessibility compliance
|
||||
• Change management facilitation and training efficiency improvement
|
||||
|
||||
Operational scenarios:
|
||||
• User onboarding, department-specific customization, and system migration
|
||||
• Multi-tenant customization and project-based configuration adjustments
|
||||
|
||||
Target users: End users, department managers, and organizations focused on optimizing
|
||||
user experience and workflow efficiency through personalization.
|
||||
""")
|
||||
public @interface SettingsApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for Team Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/team"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/team")
|
||||
@Tag(
|
||||
name = "Team",
|
||||
description =
|
||||
"""
|
||||
Team management and collaboration services for organized document processing workflows.
|
||||
|
||||
This endpoint group enables organizations to structure collaborative document processing
|
||||
activities through team-based organization and resource management.
|
||||
|
||||
Common use cases:
|
||||
• Department-based processing, project collaboration, and cross-functional coordination
|
||||
• Client-specific team isolation, temporary project teams, and training coordination
|
||||
• Compliance team coordination for regulatory processing
|
||||
|
||||
Business applications:
|
||||
• Matrix organization support, client service delivery, and cost center allocation
|
||||
• Scalable collaboration, knowledge management, and team-based quality assurance
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale processing coordination and temporary team formation
|
||||
• M&A integration, remote collaboration, and knowledge transfer management
|
||||
|
||||
Target users: Team leaders, project managers, and organizations requiring structured
|
||||
collaborative environments for document processing activities.
|
||||
""")
|
||||
public @interface TeamApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for UI Data API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/ui-data"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ui-data")
|
||||
@Tag(
|
||||
name = "UI Data",
|
||||
description =
|
||||
"""
|
||||
User interface data services for dynamic frontend applications and user experience customization.
|
||||
|
||||
This endpoint group provides data services for frontend applications to render personalized
|
||||
interfaces and deliver optimized experiences based on system configuration.
|
||||
|
||||
Common use cases:
|
||||
• Dynamic UI customization, multi-language support, and feature configuration
|
||||
• Real-time status delivery, corporate branding, and mobile optimization
|
||||
• Progressive web application (PWA) configuration
|
||||
|
||||
Business applications:
|
||||
• Brand customization, user experience optimization, and accessibility compliance
|
||||
• Multi-tenant customization, training support, and performance optimization
|
||||
|
||||
Operational scenarios:
|
||||
• Frontend deployment, UI A/B testing, and system integration
|
||||
• Mobile synchronization and offline capability enhancement
|
||||
|
||||
Target users: Frontend developers, UI/UX designers, and organizations requiring
|
||||
customizable user interfaces and optimized user experiences.
|
||||
""")
|
||||
public @interface UiDataApi {}
|
||||
@@ -0,0 +1,46 @@
|
||||
package stirling.software.common.annotations.api;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
/**
|
||||
* Combined annotation for User Management API controllers.
|
||||
* Includes @RestController, @RequestMapping("/api/v1/user"), and OpenAPI @Tag.
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
@Tag(
|
||||
name = "User",
|
||||
description =
|
||||
"""
|
||||
User management and authentication services for multi-user and enterprise environments.
|
||||
|
||||
This endpoint group provides user lifecycle management capabilities for organizations
|
||||
deploying Stirling PDF in multi-user scenarios.
|
||||
|
||||
Common use cases:
|
||||
• Employee onboarding/offboarding and corporate access control
|
||||
• Department-based permissions, regulatory compliance, and SSO integration
|
||||
• Multi-tenant deployment and guest user access management
|
||||
|
||||
Business applications:
|
||||
• Enterprise IAM integration, security governance, and cost allocation
|
||||
• Compliance reporting, workflow management, and partner collaboration
|
||||
|
||||
Operational scenarios:
|
||||
• Large-scale provisioning, automated HR integration, and emergency access
|
||||
• User migration and self-service profile maintenance
|
||||
|
||||
Target users: IT administrators, HR departments, and organizations requiring
|
||||
structured user management and enterprise identity integration.
|
||||
""")
|
||||
public @interface UserApi {}
|
||||
@@ -11,7 +11,6 @@ import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@@ -20,9 +19,7 @@ import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -60,6 +57,7 @@ public class AppConfig {
|
||||
return v2Enabled;
|
||||
}
|
||||
|
||||
/* Commented out Thymeleaf template engine bean - to be removed when frontend migration is complete
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "system.customHTMLFiles", havingValue = "true")
|
||||
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) {
|
||||
@@ -67,6 +65,7 @@ public class AppConfig {
|
||||
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader));
|
||||
return templateEngine;
|
||||
}
|
||||
*/
|
||||
|
||||
@Bean(name = "loginEnabled")
|
||||
public boolean loginEnabled() {
|
||||
@@ -259,12 +258,6 @@ public class AppConfig {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "GoogleDriveEnabled")
|
||||
@Profile("default")
|
||||
public boolean googleDriveEnabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Bean(name = "license")
|
||||
@Profile("default")
|
||||
public String licenseType() {
|
||||
|
||||
+3
-13
@@ -1,19 +1,8 @@
|
||||
package stirling.software.common.configuration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
/* Commented out entire FileFallbackTemplateResolver class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.model.InputStreamTemplateResource;
|
||||
|
||||
@Slf4j
|
||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||
@@ -57,3 +46,4 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
||||
return null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
+2
@@ -30,6 +30,7 @@ public class InstallationPathConfig {
|
||||
private static final String STATIC_PATH;
|
||||
private static final String TEMPLATES_PATH;
|
||||
private static final String SIGNATURES_PATH;
|
||||
private static final String PRIVATE_KEY_PATH;
|
||||
|
||||
static {
|
||||
BASE_PATH = initializeBasePath();
|
||||
@@ -55,6 +56,7 @@ public class InstallationPathConfig {
|
||||
STATIC_PATH = CUSTOM_FILES_PATH + "static" + File.separator;
|
||||
TEMPLATES_PATH = CUSTOM_FILES_PATH + "templates" + File.separator;
|
||||
SIGNATURES_PATH = CUSTOM_FILES_PATH + "signatures" + File.separator;
|
||||
PRIVATE_KEY_PATH = CONFIG_PATH + "db" + File.separator + "keys" + File.separator;
|
||||
}
|
||||
|
||||
private static String initializeBasePath() {
|
||||
|
||||
@@ -303,10 +303,18 @@ public class ApplicationProperties {
|
||||
|
||||
@Data
|
||||
public static class Jwt {
|
||||
<<<<<<< HEAD
|
||||
private boolean enabled = true;
|
||||
private boolean keyCleanup = true;
|
||||
private int keyRetentionDays = 7;
|
||||
private Boolean secureCookie;
|
||||
=======
|
||||
private boolean enableKeystore = true;
|
||||
private boolean enableKeyRotation = false;
|
||||
private boolean enableKeyCleanup = true;
|
||||
private int keyRetentionDays = 7;
|
||||
private boolean secureCookie;
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,7 +544,6 @@ public class ApplicationProperties {
|
||||
private boolean ssoAutoLogin;
|
||||
private boolean database;
|
||||
private CustomMetadata customMetadata = new CustomMetadata();
|
||||
private GoogleDrive googleDrive = new GoogleDrive();
|
||||
|
||||
@Data
|
||||
public static class CustomMetadata {
|
||||
@@ -555,26 +562,6 @@ public class ApplicationProperties {
|
||||
: producer;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class GoogleDrive {
|
||||
private boolean enabled;
|
||||
private String clientId;
|
||||
private String apiKey;
|
||||
private String appId;
|
||||
|
||||
public String getClientId() {
|
||||
return clientId == null || clientId.trim().isEmpty() ? "" : clientId;
|
||||
}
|
||||
|
||||
public String getApiKey() {
|
||||
return apiKey == null || apiKey.trim().isEmpty() ? "" : apiKey;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId == null || appId.trim().isEmpty() ? "" : appId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
+3
-5
@@ -1,11 +1,8 @@
|
||||
package stirling.software.common.model;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
/* Commented out entire InputStreamTemplateResource class - Thymeleaf dependency removed
|
||||
* This class will be removed when frontend migration to React is complete
|
||||
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -42,3 +39,4 @@ public class InputStreamTemplateResource implements ITemplateResource {
|
||||
return inputStream != null;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -5,6 +5,8 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import jakarta.validation.constraints.AssertTrue;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -12,7 +14,9 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@EqualsAndHashCode
|
||||
@Schema(description = "PDF file input - either upload a file or provide a server-side file ID")
|
||||
public class PDFFile {
|
||||
|
||||
@Schema(
|
||||
description = "The input PDF file",
|
||||
contentMediaType = MediaType.APPLICATION_PDF_VALUE,
|
||||
@@ -20,7 +24,14 @@ public class PDFFile {
|
||||
private MultipartFile fileInput;
|
||||
|
||||
@Schema(
|
||||
description = "File ID for server-side files (can be used instead of fileInput)",
|
||||
example = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr")
|
||||
description =
|
||||
"File ID for server-side files (can be used instead of fileInput if job was previously done on file in async mode)")
|
||||
private String fileId;
|
||||
|
||||
@AssertTrue(message = "Either fileInput or fileId must be provided")
|
||||
@Schema(hidden = true)
|
||||
private boolean isValid() {
|
||||
return (fileInput != null && (fileId == null || fileId.trim().isEmpty()))
|
||||
|| (fileId != null && !fileId.trim().isEmpty() && fileInput == null);
|
||||
}
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package stirling.software.common.service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
public interface ServerCertificateServiceInterface {
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
boolean hasServerCertificate();
|
||||
|
||||
void initializeServerCertificate();
|
||||
|
||||
KeyStore getServerKeyStore() throws Exception;
|
||||
|
||||
String getServerCertificatePassword();
|
||||
|
||||
X509Certificate getServerCertificate() throws Exception;
|
||||
|
||||
byte[] getServerCertificatePublicKey() throws Exception;
|
||||
|
||||
void uploadServerCertificate(InputStream p12Stream, String password) throws Exception;
|
||||
|
||||
void deleteServerCertificate() throws Exception;
|
||||
|
||||
ServerCertificateInfo getServerCertificateInfo() throws Exception;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
class ServerCertificateInfo {
|
||||
private final boolean exists;
|
||||
private final String subject;
|
||||
private final String issuer;
|
||||
private final Date validFrom;
|
||||
private final Date validTo;
|
||||
}
|
||||
}
|
||||
-16
@@ -112,22 +112,6 @@ class ApplicationPropertiesLogicTest {
|
||||
assertTrue(ex.getMessage().toLowerCase().contains("not supported"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void premium_google_drive_getters_return_empty_string_on_null_or_blank() {
|
||||
Premium.ProFeatures.GoogleDrive gd = new Premium.ProFeatures.GoogleDrive();
|
||||
|
||||
assertEquals("", gd.getClientId());
|
||||
assertEquals("", gd.getApiKey());
|
||||
assertEquals("", gd.getAppId());
|
||||
|
||||
gd.setClientId(" id ");
|
||||
gd.setApiKey(" key ");
|
||||
gd.setAppId(" app ");
|
||||
assertEquals(" id ", gd.getClientId());
|
||||
assertEquals(" key ", gd.getApiKey());
|
||||
assertEquals(" app ", gd.getAppId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ui_getters_return_null_for_blank() {
|
||||
ApplicationProperties.Ui ui = new ApplicationProperties.Ui();
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.core.env.Environment;
|
||||
@@ -23,7 +22,6 @@ import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.AppConfig;
|
||||
import stirling.software.common.configuration.ConfigInitializer;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
@@ -45,15 +43,13 @@ public class SPDFApplication {
|
||||
|
||||
private final AppConfig appConfig;
|
||||
private final Environment env;
|
||||
private final WebBrowser webBrowser;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
public SPDFApplication(
|
||||
AppConfig appConfig,
|
||||
Environment env,
|
||||
@Autowired(required = false) WebBrowser webBrowser) {
|
||||
AppConfig appConfig, Environment env, ApplicationProperties applicationProperties) {
|
||||
this.appConfig = appConfig;
|
||||
this.env = env;
|
||||
this.webBrowser = webBrowser;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException, InterruptedException {
|
||||
@@ -147,28 +143,31 @@ public class SPDFApplication {
|
||||
serverPortStatic = serverPort;
|
||||
String url = baseUrl + ":" + getStaticPort() + contextPath;
|
||||
|
||||
if (webBrowser != null
|
||||
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
webBrowser.initWebUI(url);
|
||||
} else {
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
// Desktop UI initialization removed - webBrowser dependency eliminated
|
||||
// Keep backwards compatibility for STIRLING_PDF_DESKTOP_UI system property
|
||||
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
|
||||
log.info("Desktop UI mode enabled, but WebBrowser functionality has been removed");
|
||||
// webBrowser.initWebUI(url); // Removed - desktop UI eliminated
|
||||
}
|
||||
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error opening browser: {}", e.getMessage());
|
||||
// Standard browser opening logic
|
||||
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
|
||||
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
|
||||
if (browserOpen) {
|
||||
try {
|
||||
String os = System.getProperty("os.name").toLowerCase();
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
|
||||
if (os.contains("win")) {
|
||||
// For Windows
|
||||
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
|
||||
} else if (os.contains("mac")) {
|
||||
SystemCommand.runCommand(rt, "open " + url);
|
||||
} else if (os.contains("nix") || os.contains("nux")) {
|
||||
SystemCommand.runCommand(rt, "xdg-open " + url);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error opening browser: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,9 +184,10 @@ public class SPDFApplication {
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (webBrowser != null) {
|
||||
webBrowser.cleanup();
|
||||
}
|
||||
// webBrowser cleanup removed - desktop UI eliminated
|
||||
// if (webBrowser != null) {
|
||||
// webBrowser.cleanup();
|
||||
// }
|
||||
}
|
||||
|
||||
private static void printStartupLogs() {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package stirling.software.SPDF.UI;
|
||||
|
||||
public interface WebBrowser {
|
||||
void initWebUI(String url);
|
||||
|
||||
void cleanup();
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.callback.CefBeforeDownloadCallback;
|
||||
import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import me.friwi.jcefmaven.CefAppBuilder;
|
||||
import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
name = "STIRLING_PDF_DESKTOP_UI",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public class DesktopBrowser implements WebBrowser {
|
||||
private static CefApp cefApp;
|
||||
private static CefClient client;
|
||||
private static CefBrowser browser;
|
||||
private static JFrame frame;
|
||||
private static LoadingWindow loadingWindow;
|
||||
private static volatile boolean browserInitialized = false;
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
loadingWindow.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void initWebUI(String url) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
builder.setInstallDir(
|
||||
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
|
||||
// Set up download handler
|
||||
setupDownloadHandler();
|
||||
|
||||
// Create browser and frame on EDT
|
||||
SwingUtilities.invokeAndWait(
|
||||
() -> {
|
||||
browser = client.createBrowser(url, false, false);
|
||||
setupMainFrame();
|
||||
setupLoadHandler();
|
||||
|
||||
// Force initialize UI after 7 seconds if not already done
|
||||
Timer timeoutTimer =
|
||||
new Timer(
|
||||
2500,
|
||||
e -> {
|
||||
log.warn(
|
||||
"Loading timeout reached. Forcing"
|
||||
+ " UI transition.");
|
||||
if (!browserInitialized) {
|
||||
// Force UI initialization
|
||||
forceInitializeUI();
|
||||
}
|
||||
});
|
||||
timeoutTimer.setRepeats(false);
|
||||
timeoutTimer.start();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing JCEF browser: ", e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
log.info("basePath {}", basePath);
|
||||
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||
// settings.browser_subprocess_path = new File(basePath +
|
||||
// "subprocess").getAbsolutePath();
|
||||
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
||||
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
||||
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
||||
|
||||
settings.persist_session_cookies = true;
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
|
||||
builder.setAppHandler(
|
||||
new MavenCefAppHandlerAdapter() {
|
||||
@Override
|
||||
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
||||
log.info("CEF state changed: {}", state);
|
||||
if (state == CefApp.CefAppState.TERMINATED) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(
|
||||
new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeDownload(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
CefBeforeDownloadCallback callback) {
|
||||
callback.Continue("", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
log.info("Download completed: {}", downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
log.info("Download canceled: {}", downloadItem.getFullPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConsoleProgressHandler createProgressHandler() {
|
||||
return new ConsoleProgressHandler() {
|
||||
@Override
|
||||
public void handleProgress(EnumProgress state, float percent) {
|
||||
Objects.requireNonNull(state, "state cannot be null");
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
switch (state) {
|
||||
case LOCATING:
|
||||
loadingWindow.setStatus("Locating Files...");
|
||||
loadingWindow.setProgress(0);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
if (percent >= 0) {
|
||||
loadingWindow.setStatus(
|
||||
String.format(
|
||||
"Downloading additional files: %.0f%%",
|
||||
percent));
|
||||
loadingWindow.setProgress((int) percent);
|
||||
}
|
||||
break;
|
||||
case EXTRACTING:
|
||||
loadingWindow.setStatus("Extracting files...");
|
||||
loadingWindow.setProgress(60);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
loadingWindow.setStatus("Initializing UI...");
|
||||
loadingWindow.setProgress(80);
|
||||
break;
|
||||
case INITIALIZED:
|
||||
loadingWindow.setStatus("Finalising startup...");
|
||||
loadingWindow.setProgress(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupMainFrame() {
|
||||
frame = new JFrame("Stirling-PDF");
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setUndecorated(true);
|
||||
frame.setOpacity(0.0f);
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.setDoubleBuffered(true);
|
||||
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
||||
frame.setContentPane(contentPane);
|
||||
|
||||
frame.addWindowListener(
|
||||
new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
final long initStartTime = System.currentTimeMillis();
|
||||
log.info("Setting up load handler at: {}", initStartTime);
|
||||
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(
|
||||
CefBrowser browser,
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
log.debug(
|
||||
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:"
|
||||
+ " {}, browserInitialized: {}, Time elapsed: {}ms",
|
||||
isLoading,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
browserInitialized,
|
||||
System.currentTimeMillis() - initStartTime);
|
||||
|
||||
if (!isLoading && !browserInitialized) {
|
||||
log.info(
|
||||
"Browser finished loading, preparing to initialize UI"
|
||||
+ " components");
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Starting UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(
|
||||
UIScaling.scaleWidth(1280),
|
||||
UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
log.info(
|
||||
"Browser component"
|
||||
+ " focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing"
|
||||
+ " browser",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during UI initialization", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTrayIcon(Image icon) {
|
||||
if (!SystemTray.isSupported()) {
|
||||
log.warn("System tray is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
systemTray = SystemTray.getSystemTray();
|
||||
|
||||
// Create popup menu
|
||||
PopupMenu popup = new PopupMenu();
|
||||
|
||||
// Create menu items
|
||||
MenuItem showItem = new MenuItem("Show");
|
||||
showItem.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(
|
||||
e -> {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
// Add menu items to popup menu
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
// Create tray icon
|
||||
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
// Add double-click behavior
|
||||
trayIcon.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
// Add tray icon to system tray
|
||||
systemTray.add(trayIcon);
|
||||
|
||||
// Modify frame behavior to minimize to tray
|
||||
frame.addWindowStateListener(
|
||||
new WindowStateListener() {
|
||||
public void windowStateChanged(WindowEvent e) {
|
||||
if (e.getNewState() == Frame.ICONIFIED) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (AWTException e) {
|
||||
log.error("Error setting up system tray icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
try {
|
||||
Image icon = null;
|
||||
String[] iconPaths = {"/static/favicon.ico"};
|
||||
|
||||
for (String path : iconPaths) {
|
||||
if (icon != null) break;
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
icon = ImageIO.read(is);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not load icon from {}", path, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
frame.setIconImage(icon);
|
||||
setupTrayIcon(icon);
|
||||
} else {
|
||||
log.warn("Could not load icon from any source");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
if (cefApp != null) cefApp.dispose();
|
||||
if (loadingWindow != null) loadingWindow.dispose();
|
||||
}
|
||||
|
||||
public static void forceInitializeUI() {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Forcing start of UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component if available
|
||||
if (browser != null) {
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent().requestFocus();
|
||||
log.info("Browser component focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing browser during force ui"
|
||||
+ " initialization.",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during Forced UI initialization.", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Slf4j
|
||||
public class LoadingWindow extends JDialog {
|
||||
private static final Pattern PATTERN =
|
||||
RegexPatternUtils.getInstance().getContainsDigitsPattern();
|
||||
private final JProgressBar progressBar;
|
||||
private final JLabel statusLabel;
|
||||
private final JPanel mainPanel;
|
||||
private final JLabel brandLabel;
|
||||
private long startTime;
|
||||
|
||||
private Timer stuckTimer;
|
||||
private long stuckThreshold = 4000;
|
||||
private long timeAt90Percent = -1;
|
||||
private volatile Process explorerProcess;
|
||||
private static final boolean IS_WINDOWS =
|
||||
System.getProperty("os.name").toLowerCase().contains("win");
|
||||
|
||||
public LoadingWindow(Frame parent, String initialUrl) {
|
||||
super(parent, "Initializing Stirling-PDF", true);
|
||||
startTime = System.currentTimeMillis();
|
||||
log.info("Creating LoadingWindow - initialization started at: {}", startTime);
|
||||
|
||||
// Initialize components
|
||||
mainPanel = new JPanel();
|
||||
mainPanel.setBackground(Color.WHITE);
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
|
||||
mainPanel.setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
|
||||
// Configure GridBagConstraints
|
||||
gbc.gridwidth = GridBagConstraints.REMAINDER;
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.weightx = 1.0;
|
||||
gbc.weighty = 0.0;
|
||||
|
||||
// Add icon
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
|
||||
if (is != null) {
|
||||
Image img = ImageIO.read(is);
|
||||
if (img != null) {
|
||||
Image scaledImg = UIScaling.scaleIcon(img, 48, 48);
|
||||
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
|
||||
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
gbc.gridy = 0;
|
||||
mainPanel.add(iconLabel, gbc);
|
||||
log.info("Icon loaded and scaled successfully");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load icon", e);
|
||||
}
|
||||
|
||||
// URL Label with explicit size
|
||||
brandLabel = new JLabel(initialUrl);
|
||||
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
brandLabel.setPreferredSize(new Dimension(300, 25));
|
||||
brandLabel.setText("Stirling-PDF");
|
||||
gbc.gridy = 1;
|
||||
mainPanel.add(brandLabel, gbc);
|
||||
|
||||
// Status label with explicit size
|
||||
statusLabel = new JLabel("Initializing...");
|
||||
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
|
||||
statusLabel.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 2;
|
||||
mainPanel.add(statusLabel, gbc);
|
||||
|
||||
// Progress bar with explicit size
|
||||
progressBar = new JProgressBar(0, 100);
|
||||
progressBar.setStringPainted(true);
|
||||
progressBar.setPreferredSize(new Dimension(300, 25));
|
||||
gbc.gridy = 3;
|
||||
mainPanel.add(progressBar, gbc);
|
||||
|
||||
// Set dialog properties
|
||||
setContentPane(mainPanel);
|
||||
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
|
||||
setResizable(false);
|
||||
setUndecorated(false);
|
||||
|
||||
// Set size and position
|
||||
setSize(UIScaling.scaleWidth(400), UIScaling.scaleHeight(200));
|
||||
|
||||
setLocationRelativeTo(parent);
|
||||
setAlwaysOnTop(true);
|
||||
setProgress(0);
|
||||
setStatus("Starting...");
|
||||
|
||||
log.info(
|
||||
"LoadingWindow initialization completed in {}ms",
|
||||
System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
private void checkAndRefreshExplorer() {
|
||||
if (!IS_WINDOWS) {
|
||||
return;
|
||||
}
|
||||
if (timeAt90Percent == -1) {
|
||||
timeAt90Percent = System.currentTimeMillis();
|
||||
stuckTimer =
|
||||
new Timer(
|
||||
1000,
|
||||
e -> {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
if (currentTime - timeAt90Percent > stuckThreshold) {
|
||||
try {
|
||||
log.debug(
|
||||
"Attempting Windows explorer refresh due to 90% stuck state");
|
||||
String currentDir = System.getProperty("user.dir");
|
||||
|
||||
// Store current explorer PIDs before we start new one
|
||||
Set<String> existingPids = new HashSet<>();
|
||||
ProcessBuilder listExplorer =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"wmic",
|
||||
"process",
|
||||
"where",
|
||||
"name='explorer.exe'",
|
||||
"get",
|
||||
"ProcessId",
|
||||
"/format:csv");
|
||||
Process process = listExplorer.start();
|
||||
BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
process.getInputStream()));
|
||||
String line;
|
||||
while ((line =
|
||||
BoundedLineReader.readLine(
|
||||
reader, 5_000_000))
|
||||
!= null) {
|
||||
if (RegexPatternUtils.getInstance()
|
||||
.getContainsDigitsPattern()
|
||||
.matcher(line)
|
||||
.matches()) { // Contains numbers
|
||||
String[] parts = line.trim().split(",");
|
||||
if (parts.length >= 2) {
|
||||
existingPids.add(
|
||||
parts[parts.length - 1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
process.waitFor(2, TimeUnit.SECONDS);
|
||||
|
||||
// Start new explorer
|
||||
ProcessBuilder pb =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"start",
|
||||
"/min",
|
||||
"/b",
|
||||
"explorer.exe",
|
||||
currentDir);
|
||||
pb.redirectErrorStream(true);
|
||||
explorerProcess = pb.start();
|
||||
|
||||
// Schedule cleanup
|
||||
Timer cleanupTimer =
|
||||
new Timer(
|
||||
2000,
|
||||
cleanup -> {
|
||||
try {
|
||||
// Find new explorer processes
|
||||
ProcessBuilder findNewExplorer =
|
||||
new ProcessBuilder(
|
||||
"cmd",
|
||||
"/c",
|
||||
"wmic",
|
||||
"process",
|
||||
"where",
|
||||
"name='explorer.exe'",
|
||||
"get",
|
||||
"ProcessId",
|
||||
"/format:csv");
|
||||
Process newProcess =
|
||||
findNewExplorer.start();
|
||||
BufferedReader newReader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(
|
||||
newProcess
|
||||
.getInputStream()));
|
||||
String newLine;
|
||||
while ((newLine =
|
||||
BoundedLineReader
|
||||
.readLine(
|
||||
newReader,
|
||||
5_000_000))
|
||||
!= null) {
|
||||
if (PATTERN.matcher(newLine)
|
||||
.matches()) {
|
||||
String[] parts =
|
||||
newLine.trim()
|
||||
.split(",");
|
||||
if (parts.length >= 2) {
|
||||
String pid =
|
||||
parts[
|
||||
parts.length
|
||||
- 1]
|
||||
.trim();
|
||||
if (!existingPids
|
||||
.contains(
|
||||
pid)) {
|
||||
log.debug(
|
||||
"Found new explorer.exe with PID: {}",
|
||||
pid);
|
||||
ProcessBuilder
|
||||
killProcess =
|
||||
new ProcessBuilder(
|
||||
"taskkill",
|
||||
"/PID",
|
||||
pid,
|
||||
"/F");
|
||||
killProcess
|
||||
.redirectErrorStream(
|
||||
true);
|
||||
Process killResult =
|
||||
killProcess
|
||||
.start();
|
||||
killResult.waitFor(
|
||||
2,
|
||||
TimeUnit
|
||||
.SECONDS);
|
||||
log.debug(
|
||||
"Explorer process terminated: {}",
|
||||
pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
newProcess.waitFor(
|
||||
2, TimeUnit.SECONDS);
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error cleaning up Windows explorer process",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
cleanupTimer.setRepeats(false);
|
||||
cleanupTimer.start();
|
||||
stuckTimer.stop();
|
||||
} catch (Exception ex) {
|
||||
log.error("Error refreshing Windows explorer", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
stuckTimer.setRepeats(true);
|
||||
stuckTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
public void setProgress(final int progress) {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
int validProgress = Math.min(Math.max(progress, 0), 100);
|
||||
log.info(
|
||||
"Setting progress to {}% at {}ms since start",
|
||||
validProgress, System.currentTimeMillis() - startTime);
|
||||
|
||||
// Log additional details when near 90%
|
||||
if (validProgress >= 85 && validProgress <= 95) {
|
||||
log.info(
|
||||
"Near 90% progress - Current status: {}, Window visible: {}, "
|
||||
+ "Progress bar responding: {}, Memory usage: {}MB",
|
||||
statusLabel.getText(),
|
||||
isVisible(),
|
||||
progressBar.isEnabled(),
|
||||
Runtime.getRuntime().totalMemory() / (1024 * 1024));
|
||||
|
||||
// Add thread state logging
|
||||
Thread currentThread = Thread.currentThread();
|
||||
log.info(
|
||||
"Current thread state - Name: {}, State: {}, Priority: {}",
|
||||
currentThread.getName(),
|
||||
currentThread.getState(),
|
||||
currentThread.getPriority());
|
||||
|
||||
if (validProgress >= 90 && validProgress < 95) {
|
||||
checkAndRefreshExplorer();
|
||||
} else {
|
||||
// Reset the timer if we move past 95%
|
||||
if (validProgress >= 95) {
|
||||
if (stuckTimer != null) {
|
||||
stuckTimer.stop();
|
||||
}
|
||||
timeAt90Percent = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressBar.setValue(validProgress);
|
||||
progressBar.setString(validProgress + "%");
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating progress to {}", progress, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatus(final String status) {
|
||||
log.info(
|
||||
"Status update at {}ms - Setting status to: {}",
|
||||
System.currentTimeMillis() - startTime,
|
||||
status);
|
||||
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
String validStatus = status != null ? status : "";
|
||||
statusLabel.setText(validStatus);
|
||||
|
||||
// Log UI state when status changes
|
||||
log.info(
|
||||
"UI State - Window visible: {}, Progress: {}%, Status: {}",
|
||||
isVisible(), progressBar.getValue(), validStatus);
|
||||
|
||||
mainPanel.revalidate();
|
||||
mainPanel.repaint();
|
||||
} catch (Exception e) {
|
||||
log.error("Error updating status to: {}", status, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
log.info("LoadingWindow disposing after {}ms", System.currentTimeMillis() - startTime);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -234,6 +234,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("PageOps", "pdf-organizer");
|
||||
addEndpointToGroup("PageOps", "rotate-pdf");
|
||||
addEndpointToGroup("PageOps", "multi-page-layout");
|
||||
addEndpointToGroup("PageOps", "booklet-imposition");
|
||||
addEndpointToGroup("PageOps", "scale-pages");
|
||||
addEndpointToGroup("PageOps", "crop");
|
||||
addEndpointToGroup("PageOps", "extract-page");
|
||||
@@ -363,6 +364,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Java", "cert-sign");
|
||||
addEndpointToGroup("Java", "remove-cert-sign");
|
||||
addEndpointToGroup("Java", "multi-page-layout");
|
||||
addEndpointToGroup("Java", "booklet-imposition");
|
||||
addEndpointToGroup("Java", "scale-pages");
|
||||
addEndpointToGroup("Java", "add-page-numbers");
|
||||
addEndpointToGroup("Java", "auto-rename");
|
||||
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.Operation;
|
||||
import io.swagger.v3.oas.models.PathItem;
|
||||
import io.swagger.v3.oas.models.media.Content;
|
||||
import io.swagger.v3.oas.models.media.MediaType;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse;
|
||||
|
||||
/**
|
||||
* Global OpenAPI customizer that adds standard error responses (400, 413, 422, 500) to all API
|
||||
* operations under /api/v1/** paths.
|
||||
*/
|
||||
@Component
|
||||
public class GlobalErrorResponseCustomizer implements GlobalOpenApiCustomizer {
|
||||
|
||||
@Override
|
||||
public void customise(OpenAPI openApi) {
|
||||
if (openApi.getPaths() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
openApi.getPaths()
|
||||
.forEach(
|
||||
(path, pathItem) -> {
|
||||
if (path.startsWith("/api/v1/")) {
|
||||
addErrorResponsesToPathItem(pathItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void addErrorResponsesToPathItem(PathItem pathItem) {
|
||||
if (pathItem.getPost() != null) {
|
||||
addStandardErrorResponses(pathItem.getPost());
|
||||
}
|
||||
if (pathItem.getPut() != null) {
|
||||
addStandardErrorResponses(pathItem.getPut());
|
||||
}
|
||||
if (pathItem.getPatch() != null) {
|
||||
addStandardErrorResponses(pathItem.getPatch());
|
||||
}
|
||||
if (pathItem.getDelete() != null) {
|
||||
addStandardErrorResponses(pathItem.getDelete());
|
||||
}
|
||||
if (pathItem.getGet() != null) {
|
||||
addStandardErrorResponses(pathItem.getGet());
|
||||
}
|
||||
}
|
||||
|
||||
private void addStandardErrorResponses(Operation operation) {
|
||||
if (operation.getResponses() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add error responses if they don't already exist
|
||||
if (!operation.getResponses().containsKey("400")) {
|
||||
operation.getResponses().addApiResponse("400", create400Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("413")) {
|
||||
operation.getResponses().addApiResponse("413", create413Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("422")) {
|
||||
operation.getResponses().addApiResponse("422", create422Response());
|
||||
}
|
||||
if (!operation.getResponses().containsKey("500")) {
|
||||
operation.getResponses().addApiResponse("500", create500Response());
|
||||
}
|
||||
}
|
||||
|
||||
private ApiResponse create400Response() {
|
||||
return new ApiResponse()
|
||||
.description(
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted file")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
400,
|
||||
"Invalid input parameters or corrupted file",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
400,
|
||||
"Invalid input parameters or corrupted file",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create413Response() {
|
||||
return new ApiResponse()
|
||||
.description("Payload too large - File exceeds maximum allowed size")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
413,
|
||||
"File size exceeds maximum allowed limit",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
413,
|
||||
"File size exceeds maximum allowed limit",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create422Response() {
|
||||
return new ApiResponse()
|
||||
.description("Unprocessable entity - File is valid but cannot be processed")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
422,
|
||||
"File is valid but cannot be processed",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
422,
|
||||
"File is valid but cannot be processed",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private ApiResponse create500Response() {
|
||||
return new ApiResponse()
|
||||
.description("Internal server error - Unexpected error during processing")
|
||||
.content(
|
||||
new Content()
|
||||
.addMediaType(
|
||||
"application/json",
|
||||
new MediaType()
|
||||
.schema(
|
||||
createErrorSchema(
|
||||
500,
|
||||
"Unexpected error during processing",
|
||||
"/api/v1/example/endpoint"))
|
||||
.example(
|
||||
createErrorExample(
|
||||
500,
|
||||
"Unexpected error during processing",
|
||||
"/api/v1/example/endpoint"))));
|
||||
}
|
||||
|
||||
private Schema<?> createErrorSchema(int status, String message, String path) {
|
||||
return new Schema<>()
|
||||
.type("object")
|
||||
.addProperty("status", new Schema<>().type("integer").example(status))
|
||||
.addProperty("error", new Schema<>().type("string").example(getErrorType(status)))
|
||||
.addProperty("message", new Schema<>().type("string").example(message))
|
||||
.addProperty(
|
||||
"timestamp",
|
||||
new Schema<>()
|
||||
.type("string")
|
||||
.format("date-time")
|
||||
.example("2024-01-15T10:30:00Z"))
|
||||
.addProperty("path", new Schema<>().type("string").example(path));
|
||||
}
|
||||
|
||||
private Object createErrorExample(int status, String message, String path) {
|
||||
return java.util.Map.of(
|
||||
"status", status,
|
||||
"error", getErrorType(status),
|
||||
"message", message,
|
||||
"timestamp", "2024-01-15T10:30:00Z",
|
||||
"path", path);
|
||||
}
|
||||
|
||||
private String getErrorType(int status) {
|
||||
return switch (status) {
|
||||
case 400 -> "Bad Request";
|
||||
case 413 -> "Payload Too Large";
|
||||
case 422 -> "Unprocessable Entity";
|
||||
case 500 -> "Internal Server Error";
|
||||
default -> "Error";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -8,8 +11,13 @@ import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.info.License;
|
||||
import io.swagger.v3.oas.models.media.ComposedSchema;
|
||||
import io.swagger.v3.oas.models.media.ObjectSchema;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.oas.models.media.StringSchema;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -41,8 +49,7 @@ public class OpenApiConfig {
|
||||
new License()
|
||||
.name("MIT")
|
||||
.url(
|
||||
"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE")
|
||||
.identifier("MIT"))
|
||||
"https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/LICENSE"))
|
||||
.termsOfService("https://www.stirlingpdf.com/terms")
|
||||
.contact(
|
||||
new Contact()
|
||||
@@ -50,18 +57,93 @@ public class OpenApiConfig {
|
||||
.url("https://www.stirlingpdf.com")
|
||||
.email("[email protected]"))
|
||||
.description(DEFAULT_DESCRIPTION);
|
||||
|
||||
OpenAPI openAPI = new OpenAPI().info(info).openapi("3.0.3");
|
||||
|
||||
// Add server configuration from environment variable
|
||||
String swaggerServerUrl = System.getenv("SWAGGER_SERVER_URL");
|
||||
if (swaggerServerUrl != null && !swaggerServerUrl.trim().isEmpty()) {
|
||||
Server server = new Server().url(swaggerServerUrl).description("API Server");
|
||||
openAPI.addServersItem(server);
|
||||
}
|
||||
|
||||
// Add ErrorResponse schema to components
|
||||
Schema<?> errorResponseSchema =
|
||||
new Schema<>()
|
||||
.type("object")
|
||||
.addProperty(
|
||||
"timestamp",
|
||||
new Schema<>()
|
||||
.type("string")
|
||||
.format("date-time")
|
||||
.description("Error timestamp"))
|
||||
.addProperty(
|
||||
"status",
|
||||
new Schema<>().type("integer").description("HTTP status code"))
|
||||
.addProperty(
|
||||
"error", new Schema<>().type("string").description("Error type"))
|
||||
.addProperty(
|
||||
"message",
|
||||
new Schema<>().type("string").description("Error message"))
|
||||
.addProperty(
|
||||
"path", new Schema<>().type("string").description("Request path"))
|
||||
.description("Standard error response format");
|
||||
|
||||
Components components = new Components().addSchemas("ErrorResponse", errorResponseSchema);
|
||||
|
||||
if (!applicationProperties.getSecurity().getEnableLogin()) {
|
||||
return new OpenAPI().components(new Components()).info(info);
|
||||
return openAPI.components(components);
|
||||
} else {
|
||||
SecurityScheme apiKeyScheme =
|
||||
new SecurityScheme()
|
||||
.type(SecurityScheme.Type.APIKEY)
|
||||
.in(SecurityScheme.In.HEADER)
|
||||
.name("X-API-KEY");
|
||||
return new OpenAPI()
|
||||
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
|
||||
.info(info)
|
||||
components.addSecuritySchemes("apiKey", apiKeyScheme);
|
||||
return openAPI.components(components)
|
||||
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
OpenApiCustomizer pdfFileOneOfCustomizer() {
|
||||
return openApi -> {
|
||||
var components = openApi.getComponents();
|
||||
var schemas = components.getSchemas();
|
||||
|
||||
// Define the two shapes
|
||||
var upload =
|
||||
new ObjectSchema()
|
||||
.name("PDFFileUpload")
|
||||
.description("Upload a PDF file")
|
||||
.addProperty("fileInput", new StringSchema().format("binary"))
|
||||
.addRequiredItem("fileInput");
|
||||
|
||||
var ref =
|
||||
new ObjectSchema()
|
||||
.name("PDFFileRef")
|
||||
.description("Reference a server-side file")
|
||||
.addProperty(
|
||||
"fileId",
|
||||
new StringSchema()
|
||||
.example("a1b2c3d4-5678-90ab-cdef-ghijklmnopqr"))
|
||||
.addRequiredItem("fileId");
|
||||
|
||||
schemas.put("PDFFileUpload", upload);
|
||||
schemas.put("PDFFileRef", ref);
|
||||
|
||||
// Create the oneOf schema
|
||||
var pdfFileOneOf =
|
||||
new ComposedSchema()
|
||||
.oneOf(
|
||||
List.of(
|
||||
new Schema<>()
|
||||
.$ref("#/components/schemas/PDFFileUpload"),
|
||||
new Schema<>().$ref("#/components/schemas/PDFFileRef")))
|
||||
.description("Either upload a file or provide a server-side file ID");
|
||||
|
||||
// Replace PDFFile schema
|
||||
schemas.put("PDFFile", pdfFileOneOf);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import org.springdoc.core.customizers.OpenApiCustomizer;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class SpringDocConfig {
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi pdfProcessingApi(
|
||||
@Qualifier("pdfFileOneOfCustomizer") OpenApiCustomizer pdfFileOneOfCustomizer) {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("file-processing")
|
||||
.displayName("File Processing")
|
||||
.pathsToMatch("/api/v1/**")
|
||||
.pathsToExclude(
|
||||
"/api/v1/admin/**",
|
||||
"/api/v1/user/**",
|
||||
"/api/v1/settings/**",
|
||||
"/api/v1/team/**",
|
||||
"/api/v1/ui-data/**",
|
||||
"/api/v1/proprietary/ui-data/**",
|
||||
"/api/v1/info/**",
|
||||
"/api/v1/general/job/**",
|
||||
"/api/v1/general/files/**")
|
||||
.addOpenApiCustomizer(pdfFileOneOfCustomizer)
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - Processing API")
|
||||
.description(
|
||||
"API documentation for PDF processing operations including conversion, manipulation, security, and utilities."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi adminApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("management")
|
||||
.displayName("Management")
|
||||
.pathsToMatch(
|
||||
"/api/v1/admin/**",
|
||||
"/api/v1/user/**",
|
||||
"/api/v1/settings/**",
|
||||
"/api/v1/team/**")
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - Admin API")
|
||||
.description(
|
||||
"API documentation for administrative functions, user management, and system configuration."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi systemApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("system")
|
||||
.displayName("System & UI API")
|
||||
.pathsToMatch(
|
||||
"/api/v1/ui-data/**",
|
||||
"/api/v1/proprietary/ui-data/**",
|
||||
"/api/v1/info/**",
|
||||
"/api/v1/general/job/**",
|
||||
"/api/v1/general/files/**")
|
||||
.addOpenApiCustomizer(
|
||||
openApi -> {
|
||||
openApi.info(
|
||||
openApi.getInfo()
|
||||
.title("Stirling PDF - System API")
|
||||
.description(
|
||||
"API documentation for system information, UI data, and utility endpoints."));
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,10 @@ package stirling.software.SPDF.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@@ -20,12 +17,12 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
registry.addInterceptor(endpointInterceptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// Handler for external static resources
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations(
|
||||
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// .setCachePeriod(0); // Optional: disable caching
|
||||
}
|
||||
// @Override
|
||||
// public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// // Handler for external static resources - DISABLED in backend-only mode
|
||||
// registry.addResourceHandler("/**")
|
||||
// .addResourceLocations(
|
||||
// "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
|
||||
// // .setCachePeriod(0); // Optional: disable caching
|
||||
// }
|
||||
}
|
||||
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to CSV conversions. Use for endpoints that convert PDF tables to
|
||||
* CSV format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF tables extracted successfully to CSV format",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "text/csv",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"CSV file containing extracted table data")),
|
||||
@Content(
|
||||
mediaType = "application/zip",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"ZIP archive containing multiple CSV files when multiple tables are extracted"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or no tables found for extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(responseCode = "204", description = "No tables found in the PDF"),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during table extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface CsvConversionResponse {}
|
||||
@@ -0,0 +1,30 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Schema(description = "Standard error response")
|
||||
public class ErrorResponse {
|
||||
|
||||
@Schema(description = "HTTP status code", example = "400")
|
||||
private int status;
|
||||
|
||||
@Schema(
|
||||
description = "Error message describing what went wrong",
|
||||
example = "Invalid PDF file or corrupted data")
|
||||
private String message;
|
||||
|
||||
@Schema(description = "Timestamp when the error occurred", example = "2024-01-15T10:30:00Z")
|
||||
private String timestamp;
|
||||
|
||||
@Schema(
|
||||
description = "Request path where the error occurred",
|
||||
example = "/api/v1/{endpoint-path}")
|
||||
private String path;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for filter operations that conditionally return the original file. Use
|
||||
* for operations like text filters, page count filters, size filters, etc. Returns the original PDF
|
||||
* if condition is met, otherwise returns no content (204).
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Filter condition met - returns the original PDF file",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "The original PDF file"))),
|
||||
@ApiResponse(
|
||||
responseCode = "204",
|
||||
description = "Filter condition not met - no content returned",
|
||||
content = @Content()),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - Invalid filter parameters or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be analyzed for filtering",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during PDF analysis",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface FilterResponse {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to HTML conversions. Use for endpoints that convert PDF to HTML
|
||||
* format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to HTML format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/html",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "HTML file converted from PDF")))
|
||||
})
|
||||
public @interface HtmlConversionResponse {}
|
||||
@@ -0,0 +1,34 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for JavaScript extraction from PDFs. Use for endpoints that extract
|
||||
* JavaScript code from PDF documents.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "JavaScript extracted successfully from PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"JavaScript code extracted from PDF")))
|
||||
})
|
||||
public @interface JavaScriptResponse {}
|
||||
@@ -0,0 +1,47 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for operations that return JSON data or analysis results. Use for
|
||||
* analysis operations, metadata extraction, info operations, etc.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "Analysis or data extraction completed successfully",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "object",
|
||||
description =
|
||||
"JSON object containing the requested data or analysis results"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface JsonDataResponse {}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to Markdown conversions. Use for endpoints that convert PDF to
|
||||
* Markdown format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to Markdown format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "text/markdown",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"Markdown file converted from PDF")))
|
||||
})
|
||||
public @interface MarkdownConversionResponse {}
|
||||
@@ -0,0 +1,71 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for operations that may return multiple files or a ZIP archive. Use for
|
||||
* operations like PDF to images, split PDF, or multiple file conversions.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description =
|
||||
"Files processed successfully. Returns single file or ZIP archive containing multiple files.",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single PDF file result")),
|
||||
@Content(
|
||||
mediaType = "application/zip",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"ZIP archive containing multiple output files")),
|
||||
@Content(
|
||||
mediaType = "image/png",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single image file (PNG)")),
|
||||
@Content(
|
||||
mediaType = "image/jpeg",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Single image file (JPEG)"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface MultiFileResponse {}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to PowerPoint presentation conversions. Use for endpoints that
|
||||
* convert PDF to PPTX format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to PowerPoint presentation",
|
||||
content =
|
||||
@Content(
|
||||
mediaType =
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description =
|
||||
"Microsoft PowerPoint presentation (PPTX)"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description =
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be converted to PowerPoint format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description =
|
||||
"Internal server error - Unexpected error during PowerPoint conversion",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface PowerPointConversionResponse {}
|
||||
@@ -0,0 +1,47 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* Standard API response annotation for PDF operations that take PDF input and return PDF output.
|
||||
* Use for single PDF input → single PDF output (SISO) operations like rotate, compress, etc.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF processed successfully",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/pdf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "The processed PDF file"))),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Invalid PDF file or request parameters",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error during processing",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface StandardPdfResponse {}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to plain text conversions. Use for endpoints that extract text
|
||||
* content from PDF.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF text extracted successfully",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType = "text/plain",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
description =
|
||||
"Plain text content extracted from PDF")),
|
||||
@Content(
|
||||
mediaType = "application/rtf",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Rich Text Format document"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Bad request - Invalid input parameters or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description = "Unprocessable entity - PDF is valid but text extraction failed",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during text extraction",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface TextPlainConversionResponse {}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to Word document conversions. Use for endpoints that convert PDF
|
||||
* to DOCX/DOC formats.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to Word document",
|
||||
content = {
|
||||
@Content(
|
||||
mediaType =
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Microsoft Word document (DOCX)")),
|
||||
@Content(
|
||||
mediaType = "application/msword",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "Microsoft Word document (DOC)"))
|
||||
}),
|
||||
@ApiResponse(
|
||||
responseCode = "400",
|
||||
description =
|
||||
"Bad request - Invalid input parameters, unsupported format, or corrupted PDF",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "413",
|
||||
description = "Payload too large - File exceeds maximum allowed size",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "422",
|
||||
description =
|
||||
"Unprocessable entity - PDF is valid but cannot be converted to Word format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class))),
|
||||
@ApiResponse(
|
||||
responseCode = "500",
|
||||
description = "Internal server error - Unexpected error during Word conversion",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = ErrorResponse.class)))
|
||||
})
|
||||
public @interface WordConversionResponse {}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package stirling.software.SPDF.config.swagger;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||
|
||||
/**
|
||||
* API response annotation for PDF to XML conversions. Use for endpoints that convert PDF to XML
|
||||
* format.
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "PDF converted successfully to XML format",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/xml",
|
||||
schema =
|
||||
@Schema(
|
||||
type = "string",
|
||||
format = "binary",
|
||||
description = "XML file converted from PDF")))
|
||||
})
|
||||
public @interface XmlConversionResponse {}
|
||||
+20
-12
@@ -15,22 +15,23 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.AnalysisApi;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/analysis")
|
||||
@Tag(name = "Analysis", description = "Analysis APIs")
|
||||
@AnalysisApi
|
||||
@RequiredArgsConstructor
|
||||
public class AnalysisController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/page-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/page-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF page count",
|
||||
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -40,7 +41,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/basic-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/basic-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get basic PDF information",
|
||||
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -54,7 +56,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/document-properties", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/document-properties", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF document properties",
|
||||
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -77,7 +80,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/page-dimensions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/page-dimensions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get page dimensions for all pages",
|
||||
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -97,7 +101,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/form-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/form-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get form field information",
|
||||
description =
|
||||
@@ -120,7 +125,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/annotation-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/annotation-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get annotation information",
|
||||
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
|
||||
@@ -144,7 +150,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/font-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/font-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get font information",
|
||||
description =
|
||||
@@ -166,7 +173,8 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(value = "/security-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/security-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get security information",
|
||||
description =
|
||||
|
||||
+324
@@ -0,0 +1,324 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.multipdf.LayerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class BookletImpositionController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@AutoJobPostMapping(value = "/booklet-imposition", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Create a booklet with proper page imposition",
|
||||
description =
|
||||
"This operation combines page reordering for booklet printing with multi-page layout. "
|
||||
+ "It rearranges pages in the correct order for booklet printing and places multiple pages "
|
||||
+ "on each sheet for proper folding and binding. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> createBookletImposition(
|
||||
@ModelAttribute BookletImpositionRequest request) throws IOException {
|
||||
|
||||
MultipartFile file = request.getFileInput();
|
||||
int pagesPerSheet = request.getPagesPerSheet();
|
||||
boolean addBorder = Boolean.TRUE.equals(request.getAddBorder());
|
||||
String spineLocation =
|
||||
request.getSpineLocation() != null ? request.getSpineLocation() : "LEFT";
|
||||
boolean addGutter = Boolean.TRUE.equals(request.getAddGutter());
|
||||
float gutterSize = request.getGutterSize();
|
||||
boolean doubleSided = Boolean.TRUE.equals(request.getDoubleSided());
|
||||
String duplexPass = request.getDuplexPass() != null ? request.getDuplexPass() : "BOTH";
|
||||
boolean flipOnShortEdge = Boolean.TRUE.equals(request.getFlipOnShortEdge());
|
||||
|
||||
// Validate pages per sheet for booklet - only 2-up landscape is proper booklet
|
||||
if (pagesPerSheet != 2) {
|
||||
throw new IllegalArgumentException(
|
||||
"Booklet printing uses 2 pages per side (landscape). For 4-up, use the N-up feature.");
|
||||
}
|
||||
|
||||
PDDocument sourceDocument = pdfDocumentFactory.load(file);
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
|
||||
// Create proper booklet with signature-based page ordering
|
||||
PDDocument newDocument =
|
||||
createSaddleBooklet(
|
||||
sourceDocument,
|
||||
totalPages,
|
||||
addBorder,
|
||||
spineLocation,
|
||||
addGutter,
|
||||
gutterSize,
|
||||
doubleSided,
|
||||
duplexPass,
|
||||
flipOnShortEdge);
|
||||
|
||||
sourceDocument.close();
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
newDocument.save(baos);
|
||||
newDocument.close();
|
||||
|
||||
byte[] result = baos.toByteArray();
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
result,
|
||||
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||
+ "_booklet.pdf");
|
||||
}
|
||||
|
||||
private static int padToMultipleOf4(int n) {
|
||||
return (n + 3) / 4 * 4;
|
||||
}
|
||||
|
||||
private static class Side {
|
||||
final int left, right;
|
||||
final boolean isBack;
|
||||
|
||||
Side(int left, int right, boolean isBack) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.isBack = isBack;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Side> saddleStitchSides(
|
||||
int totalPagesOriginal,
|
||||
boolean doubleSided,
|
||||
String duplexPass,
|
||||
boolean flipOnShortEdge) {
|
||||
int N = padToMultipleOf4(totalPagesOriginal);
|
||||
List<Side> out = new ArrayList<>();
|
||||
int sheets = N / 4;
|
||||
|
||||
for (int s = 0; s < sheets; s++) {
|
||||
int a = N - 1 - (s * 2); // left, front
|
||||
int b = (s * 2); // right, front
|
||||
int c = (s * 2) + 1; // left, back
|
||||
int d = N - 2 - (s * 2); // right, back
|
||||
|
||||
// clamp to -1 (blank) if >= totalPagesOriginal
|
||||
a = (a < totalPagesOriginal) ? a : -1;
|
||||
b = (b < totalPagesOriginal) ? b : -1;
|
||||
c = (c < totalPagesOriginal) ? c : -1;
|
||||
d = (d < totalPagesOriginal) ? d : -1;
|
||||
|
||||
// Handle duplex pass selection
|
||||
boolean includeFront = "BOTH".equals(duplexPass) || "FIRST".equals(duplexPass);
|
||||
boolean includeBack = "BOTH".equals(duplexPass) || "SECOND".equals(duplexPass);
|
||||
|
||||
if (includeFront) {
|
||||
out.add(new Side(a, b, false)); // front side
|
||||
}
|
||||
|
||||
if (includeBack) {
|
||||
// For short-edge duplex, swap back-side left/right
|
||||
// Note: flipOnShortEdge is ignored in manual duplex mode since users physically
|
||||
// flip the stack
|
||||
if (doubleSided && flipOnShortEdge) {
|
||||
out.add(new Side(d, c, true)); // swapped back side (automatic duplex only)
|
||||
} else {
|
||||
out.add(new Side(c, d, true)); // normal back side
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private PDDocument createSaddleBooklet(
|
||||
PDDocument src,
|
||||
int totalPages,
|
||||
boolean addBorder,
|
||||
String spineLocation,
|
||||
boolean addGutter,
|
||||
float gutterSize,
|
||||
boolean doubleSided,
|
||||
String duplexPass,
|
||||
boolean flipOnShortEdge)
|
||||
throws IOException {
|
||||
|
||||
PDDocument dst = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(src);
|
||||
|
||||
// Derive paper size from source document's first page CropBox
|
||||
PDRectangle srcBox = src.getPage(0).getCropBox();
|
||||
PDRectangle portraitPaper = new PDRectangle(srcBox.getWidth(), srcBox.getHeight());
|
||||
// Force landscape for booklet (Acrobat booklet uses landscape paper to fold to portrait)
|
||||
PDRectangle pageSize = new PDRectangle(portraitPaper.getHeight(), portraitPaper.getWidth());
|
||||
|
||||
// Validate and clamp gutter size
|
||||
if (gutterSize < 0) gutterSize = 0;
|
||||
if (gutterSize >= pageSize.getWidth() / 2f) gutterSize = pageSize.getWidth() / 2f - 1f;
|
||||
|
||||
List<Side> sides = saddleStitchSides(totalPages, doubleSided, duplexPass, flipOnShortEdge);
|
||||
|
||||
for (Side side : sides) {
|
||||
PDPage out = new PDPage(pageSize);
|
||||
dst.addPage(out);
|
||||
|
||||
float cellW = pageSize.getWidth() / 2f;
|
||||
float cellH = pageSize.getHeight();
|
||||
|
||||
// For RIGHT spine (RTL), swap left/right placements
|
||||
boolean rtl = "RIGHT".equalsIgnoreCase(spineLocation);
|
||||
int leftCol = rtl ? 1 : 0;
|
||||
int rightCol = rtl ? 0 : 1;
|
||||
|
||||
// Apply gutter margins with centered gap option
|
||||
float g = addGutter ? gutterSize : 0f;
|
||||
float leftCellX = leftCol * cellW + (g / 2f);
|
||||
float rightCellX = rightCol * cellW - (g / 2f);
|
||||
float leftCellW = cellW - (g / 2f);
|
||||
float rightCellW = cellW - (g / 2f);
|
||||
|
||||
// Create LayerUtility once per page for efficiency
|
||||
LayerUtility layerUtility = new LayerUtility(dst);
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(
|
||||
dst, out, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||
|
||||
if (addBorder) {
|
||||
cs.setLineWidth(1.5f);
|
||||
cs.setStrokingColor(Color.BLACK);
|
||||
}
|
||||
|
||||
// draw left cell
|
||||
drawCell(
|
||||
src,
|
||||
dst,
|
||||
cs,
|
||||
layerUtility,
|
||||
side.left,
|
||||
leftCellX,
|
||||
0f,
|
||||
leftCellW,
|
||||
cellH,
|
||||
addBorder);
|
||||
// draw right cell
|
||||
drawCell(
|
||||
src,
|
||||
dst,
|
||||
cs,
|
||||
layerUtility,
|
||||
side.right,
|
||||
rightCellX,
|
||||
0f,
|
||||
rightCellW,
|
||||
cellH,
|
||||
addBorder);
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
private void drawCell(
|
||||
PDDocument src,
|
||||
PDDocument dst,
|
||||
PDPageContentStream cs,
|
||||
LayerUtility layerUtility,
|
||||
int pageIndex,
|
||||
float cellX,
|
||||
float cellY,
|
||||
float cellW,
|
||||
float cellH,
|
||||
boolean addBorder)
|
||||
throws IOException {
|
||||
|
||||
if (pageIndex < 0) {
|
||||
// Draw border for blank cell if needed
|
||||
if (addBorder) {
|
||||
cs.addRect(cellX, cellY, cellW, cellH);
|
||||
cs.stroke();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PDPage srcPage = src.getPage(pageIndex);
|
||||
PDRectangle r = srcPage.getCropBox(); // Use CropBox instead of MediaBox
|
||||
int rot = (srcPage.getRotation() + 360) % 360;
|
||||
|
||||
// Calculate scale factors, accounting for rotation
|
||||
float sx = cellW / r.getWidth();
|
||||
float sy = cellH / r.getHeight();
|
||||
float s = Math.min(sx, sy);
|
||||
|
||||
// If rotated 90/270 degrees, swap dimensions for fitting
|
||||
if (rot == 90 || rot == 270) {
|
||||
sx = cellW / r.getHeight();
|
||||
sy = cellH / r.getWidth();
|
||||
s = Math.min(sx, sy);
|
||||
}
|
||||
|
||||
float drawnW = (rot == 90 || rot == 270) ? r.getHeight() * s : r.getWidth() * s;
|
||||
float drawnH = (rot == 90 || rot == 270) ? r.getWidth() * s : r.getHeight() * s;
|
||||
|
||||
// Center in cell, accounting for CropBox offset
|
||||
float tx = cellX + (cellW - drawnW) / 2f - r.getLowerLeftX() * s;
|
||||
float ty = cellY + (cellH - drawnH) / 2f - r.getLowerLeftY() * s;
|
||||
|
||||
cs.saveGraphicsState();
|
||||
cs.transform(Matrix.getTranslateInstance(tx, ty));
|
||||
cs.transform(Matrix.getScaleInstance(s, s));
|
||||
|
||||
// Apply rotation if needed (rotate about origin), then translate to keep in cell
|
||||
switch (rot) {
|
||||
case 90:
|
||||
cs.transform(Matrix.getRotateInstance(Math.PI / 2, 0, 0));
|
||||
// After 90° CCW, the content spans x in [-r.getHeight(), 0] and y in [0,
|
||||
// r.getWidth()]
|
||||
cs.transform(Matrix.getTranslateInstance(0, -r.getWidth()));
|
||||
break;
|
||||
case 180:
|
||||
cs.transform(Matrix.getRotateInstance(Math.PI, 0, 0));
|
||||
cs.transform(Matrix.getTranslateInstance(-r.getWidth(), -r.getHeight()));
|
||||
break;
|
||||
case 270:
|
||||
cs.transform(Matrix.getRotateInstance(3 * Math.PI / 2, 0, 0));
|
||||
// After 270° CCW, the content spans x in [0, r.getHeight()] and y in
|
||||
// [-r.getWidth(), 0]
|
||||
cs.transform(Matrix.getTranslateInstance(-r.getHeight(), 0));
|
||||
break;
|
||||
default:
|
||||
// 0°: no-op
|
||||
}
|
||||
|
||||
// Reuse LayerUtility passed from caller
|
||||
PDFormXObject form = layerUtility.importPageAsForm(src, pageIndex);
|
||||
cs.drawForm(form);
|
||||
|
||||
cs.restoreGraphicsState();
|
||||
|
||||
// Draw border on top of form to ensure visibility
|
||||
if (addBorder) {
|
||||
cs.addRect(cellX, cellY, cellW, cellH);
|
||||
cs.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,32 +16,30 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class CropController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Crops a PDF document",
|
||||
description =
|
||||
|
||||
+14
-6
@@ -14,35 +14,42 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class EditTableOfContentsController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@PostMapping(value = "/extract-bookmarks", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/extract-bookmarks", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@@ -150,7 +157,8 @@ public class EditTableOfContentsController {
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/edit-table-of-contents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/edit-table-of-contents", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
|
||||
@@ -2,7 +2,14 @@ package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
<<<<<<< HEAD
|
||||
import java.io.InputStream;
|
||||
=======
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
@@ -19,26 +26,35 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDField;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
|
||||
<<<<<<< HEAD
|
||||
import org.apache.xmpbox.XMPMetadata;
|
||||
import org.apache.xmpbox.schema.XMPBasicSchema;
|
||||
import org.apache.xmpbox.xml.DomXmpParser;
|
||||
=======
|
||||
import org.springframework.http.HttpStatus;
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
<<<<<<< HEAD
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
=======
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -47,10 +63,8 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class MergeController {
|
||||
|
||||
@@ -142,6 +156,32 @@ public class MergeController {
|
||||
};
|
||||
}
|
||||
|
||||
// Parse client file IDs from JSON string
|
||||
private String[] parseClientFileIds(String clientFileIds) {
|
||||
if (clientFileIds == null || clientFileIds.trim().isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
try {
|
||||
// Simple JSON array parsing - remove brackets and split by comma
|
||||
String trimmed = clientFileIds.trim();
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
String inside = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||
if (inside.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
String[] parts = inside.split(",");
|
||||
String[] result = new String[parts.length];
|
||||
for (int i = 0; i < parts.length; i++) {
|
||||
result[i] = parts[i].trim().replaceAll("^\"|\"$", "");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse client file IDs: {}", clientFileIds, e);
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// Adds a table of contents to the merged document using filenames as chapter titles
|
||||
private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) {
|
||||
// Create the document outline
|
||||
@@ -228,7 +268,8 @@ public class MergeController {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/merge-pdfs")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/merge-pdfs")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Merge multiple PDF files into one",
|
||||
description =
|
||||
@@ -266,12 +307,23 @@ public class MergeController {
|
||||
|
||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||
long totalSize = 0;
|
||||
for (MultipartFile multipartFile : files) {
|
||||
List<Integer> invalidIndexes = new ArrayList<>();
|
||||
for (int index = 0; index < files.length; index++) {
|
||||
MultipartFile multipartFile = files[index];
|
||||
totalSize += multipartFile.getSize();
|
||||
File tempFile =
|
||||
tempFileManager.convertMultipartFileToFile(
|
||||
multipartFile); // Convert MultipartFile to File
|
||||
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
||||
|
||||
// Pre-validate each PDF so we can report which one(s) are broken
|
||||
// Use the original MultipartFile to avoid deleting the tempFile during validation
|
||||
try (PDDocument ignored = pdfDocumentFactory.load(multipartFile)) {
|
||||
// OK
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF pre-validate", e);
|
||||
invalidIndexes.add(index);
|
||||
}
|
||||
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
||||
}
|
||||
|
||||
|
||||
+6
-8
@@ -14,33 +14,31 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class MultiPageLayoutController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/multi-page-layout", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/multi-page-layout", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Merge multiple pages of a PDF document into a single page",
|
||||
description =
|
||||
|
||||
+7
-9
@@ -7,16 +7,15 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.service.PdfImageRemovalService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -26,9 +25,7 @@ import stirling.software.common.util.WebResponseUtils;
|
||||
* Controller class for handling PDF image removal requests. Provides an endpoint to remove images
|
||||
* from a PDF file to reduce its size.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class PdfImageRemovalController {
|
||||
|
||||
@@ -48,12 +45,13 @@ public class PdfImageRemovalController {
|
||||
* content type and filename.
|
||||
* @throws IOException If an error occurs while processing the PDF file.
|
||||
*/
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove images from file to reduce the file size.",
|
||||
description =
|
||||
"This endpoint remove images from file to reduce the file size.Input:PDF"
|
||||
+ " Output:PDF Type:MISO")
|
||||
+ " Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
|
||||
// Load the PDF document
|
||||
PDDocument document = pdfDocumentFactory.load(file);
|
||||
|
||||
+6
-8
@@ -15,30 +15,28 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class PdfOverlayController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Overlay PDF files in various modes",
|
||||
description =
|
||||
|
||||
+8
-9
@@ -10,35 +10,33 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.SortTypes;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class RearrangePagesPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove pages from a PDF file",
|
||||
description =
|
||||
@@ -224,7 +222,8 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rearrange-pages")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rearrange-pages")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Rearrange pages in a PDF file",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -8,31 +8,29 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class RotationController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Rotate a PDF file",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -15,31 +15,29 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class ScalePagesController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Change the size of a PDF page/document",
|
||||
description =
|
||||
|
||||
+4
-8
@@ -5,25 +5,21 @@ import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SettingsApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Controller
|
||||
@Tag(name = "Settings", description = "Settings APIs")
|
||||
@RequestMapping("/api/v1/settings")
|
||||
@SettingsApi
|
||||
@RequiredArgsConstructor
|
||||
@Hidden
|
||||
public class SettingsController {
|
||||
@@ -31,7 +27,7 @@ public class SettingsController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@PostMapping("/update-enable-analytics")
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
|
||||
if (applicationProperties.getSystem().getEnableAnalytics() != null) {
|
||||
|
||||
+6
-8
@@ -14,18 +14,17 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -33,17 +32,16 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class SplitPDFController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split a PDF file into separate documents",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -15,13 +15,9 @@ import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlin
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -30,7 +26,10 @@ import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
@@ -38,10 +37,8 @@ import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class SplitPdfByChaptersController {
|
||||
|
||||
@@ -117,7 +114,8 @@ public class SplitPdfByChaptersController {
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
@PostMapping(value = "/split-pdf-by-chapters", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/split-pdf-by-chapters", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split PDFs by Chapters",
|
||||
description = "Splits a PDF into chapters and returns a ZIP file.")
|
||||
|
||||
+6
-8
@@ -20,18 +20,17 @@ import org.apache.pdfbox.util.Matrix;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PDFService;
|
||||
@@ -39,9 +38,7 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class SplitPdfBySectionsController {
|
||||
|
||||
@@ -49,7 +46,8 @@ public class SplitPdfBySectionsController {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final PDFService pdfService;
|
||||
|
||||
@PostMapping(value = "/split-pdf-by-sections", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/split-pdf-by-sections", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split PDF pages into smaller sections",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -12,18 +12,17 @@ import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -31,17 +30,16 @@ import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class SplitPdfBySizeController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(value = "/split-by-size-or-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/split-by-size-or-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -13,29 +13,27 @@ import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/general")
|
||||
@Tag(name = "General", description = "General APIs")
|
||||
@GeneralApi
|
||||
@RequiredArgsConstructor
|
||||
public class ToSinglePageController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf-to-single-page")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf-to-single-page")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||
description =
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.Dependency;
|
||||
import stirling.software.SPDF.model.SignatureFile;
|
||||
import stirling.software.SPDF.service.SignatureService;
|
||||
import stirling.software.common.annotations.api.UiDataApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.UserServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@Slf4j
|
||||
@UiDataApi
|
||||
public class UIDataController {
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SignatureService signatureService;
|
||||
private final UserServiceInterface userService;
|
||||
private final ResourceLoader resourceLoader;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
public UIDataController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SignatureService signatureService,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
ResourceLoader resourceLoader,
|
||||
RuntimePathConfig runtimePathConfig) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.signatureService = signatureService;
|
||||
this.userService = userService;
|
||||
this.resourceLoader = resourceLoader;
|
||||
this.runtimePathConfig = runtimePathConfig;
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
@Operation(summary = "Get home page data")
|
||||
public ResponseEntity<HomeData> getHomeData() {
|
||||
String showSurvey = System.getenv("SHOW_SURVEY");
|
||||
boolean showSurveyValue = showSurvey == null || "true".equalsIgnoreCase(showSurvey);
|
||||
|
||||
HomeData data = new HomeData();
|
||||
data.setShowSurveyFromDocker(showSurveyValue);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/licenses")
|
||||
@Operation(summary = "Get third-party licenses data")
|
||||
public ResponseEntity<LicensesData> getLicensesData() {
|
||||
LicensesData data = new LicensesData();
|
||||
Resource resource = new ClassPathResource("static/3rdPartyLicenses.json");
|
||||
|
||||
try {
|
||||
InputStream is = resource.getInputStream();
|
||||
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
Map<String, List<Dependency>> licenseData =
|
||||
mapper.readValue(json, new TypeReference<>() {});
|
||||
data.setDependencies(licenseData.get("dependencies"));
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load licenses data", e);
|
||||
data.setDependencies(Collections.emptyList());
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/pipeline")
|
||||
@Operation(summary = "Get pipeline configuration data")
|
||||
public ResponseEntity<PipelineData> getPipelineData() {
|
||||
PipelineData data = new PipelineData();
|
||||
List<String> pipelineConfigs = new ArrayList<>();
|
||||
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
|
||||
|
||||
if (new java.io.File(runtimePathConfig.getPipelineDefaultWebUiConfigs()).exists()) {
|
||||
try (Stream<Path> paths =
|
||||
Files.walk(Paths.get(runtimePathConfig.getPipelineDefaultWebUiConfigs()))) {
|
||||
List<Path> jsonFiles =
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".json"))
|
||||
.toList();
|
||||
|
||||
for (Path jsonFile : jsonFiles) {
|
||||
String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
|
||||
pipelineConfigs.add(content);
|
||||
}
|
||||
|
||||
for (String config : pipelineConfigs) {
|
||||
Map<String, Object> jsonContent =
|
||||
new ObjectMapper()
|
||||
.readValue(config, new TypeReference<Map<String, Object>>() {});
|
||||
String name = (String) jsonContent.get("name");
|
||||
if (name == null || name.length() < 1) {
|
||||
String filename =
|
||||
jsonFiles
|
||||
.get(pipelineConfigs.indexOf(config))
|
||||
.getFileName()
|
||||
.toString();
|
||||
name = filename.substring(0, filename.lastIndexOf('.'));
|
||||
}
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", config);
|
||||
configWithName.put("name", name);
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load pipeline configs", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (pipelineConfigsWithNames.isEmpty()) {
|
||||
Map<String, String> configWithName = new HashMap<>();
|
||||
configWithName.put("json", "");
|
||||
configWithName.put("name", "No preloaded configs found");
|
||||
pipelineConfigsWithNames.add(configWithName);
|
||||
}
|
||||
|
||||
data.setPipelineConfigsWithNames(pipelineConfigsWithNames);
|
||||
data.setPipelineConfigs(pipelineConfigs);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/sign")
|
||||
@Operation(summary = "Get signature form data")
|
||||
public ResponseEntity<SignData> getSignData() {
|
||||
String username = "";
|
||||
if (userService != null) {
|
||||
username = userService.getCurrentUsername();
|
||||
}
|
||||
|
||||
List<SignatureFile> signatures = signatureService.getAvailableSignatures(username);
|
||||
List<FontResource> fonts = getFontNames();
|
||||
|
||||
SignData data = new SignData();
|
||||
data.setSignatures(signatures);
|
||||
data.setFonts(fonts);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
@GetMapping("/ocr-pdf")
|
||||
@Operation(summary = "Get OCR PDF data")
|
||||
public ResponseEntity<OcrData> getOcrPdfData() {
|
||||
List<String> languages = getAvailableTesseractLanguages();
|
||||
|
||||
OcrData data = new OcrData();
|
||||
data.setLanguages(languages);
|
||||
|
||||
return ResponseEntity.ok(data);
|
||||
}
|
||||
|
||||
private List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
java.io.File[] files = new java.io.File(tessdataDir).listFiles();
|
||||
if (files == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Arrays.stream(files)
|
||||
.filter(file -> file.getName().endsWith(".traineddata"))
|
||||
.map(file -> file.getName().replace(".traineddata", ""))
|
||||
.filter(lang -> !"osd".equalsIgnoreCase(lang))
|
||||
.sorted()
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNames() {
|
||||
List<FontResource> fontNames = new ArrayList<>();
|
||||
fontNames.addAll(getFontNamesFromLocation("classpath:static/fonts/*.woff2"));
|
||||
fontNames.addAll(
|
||||
getFontNamesFromLocation(
|
||||
"file:"
|
||||
+ InstallationPathConfig.getStaticPath()
|
||||
+ "fonts"
|
||||
+ java.io.File.separator
|
||||
+ "*"));
|
||||
return fontNames;
|
||||
}
|
||||
|
||||
private List<FontResource> getFontNamesFromLocation(String locationPattern) {
|
||||
try {
|
||||
Resource[] resources =
|
||||
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
|
||||
return Arrays.stream(resources)
|
||||
.map(
|
||||
resource -> {
|
||||
try {
|
||||
String filename = resource.getFilename();
|
||||
if (filename != null) {
|
||||
int lastDotIndex = filename.lastIndexOf('.');
|
||||
if (lastDotIndex != -1) {
|
||||
String name = filename.substring(0, lastDotIndex);
|
||||
String extension = filename.substring(lastDotIndex + 1);
|
||||
return new FontResource(name, extension);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontLoadingFailed",
|
||||
"Error processing font file",
|
||||
e);
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
} catch (Exception e) {
|
||||
throw ExceptionUtils.createRuntimeException(
|
||||
"error.fontDirectoryReadFailed", "Failed to read font directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Data classes
|
||||
@Data
|
||||
public static class HomeData {
|
||||
private boolean showSurveyFromDocker;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class LicensesData {
|
||||
private List<Dependency> dependencies;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class PipelineData {
|
||||
private List<Map<String, String>> pipelineConfigsWithNames;
|
||||
private List<String> pipelineConfigs;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SignData {
|
||||
private List<SignatureFile> signatures;
|
||||
private List<FontResource> fonts;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class OcrData {
|
||||
private List<String> languages;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class FontResource {
|
||||
private String name;
|
||||
private String extension;
|
||||
private String type;
|
||||
|
||||
public FontResource(String name, String extension) {
|
||||
this.name = name;
|
||||
this.extension = extension;
|
||||
this.type = getFormatFromExtension(extension);
|
||||
}
|
||||
|
||||
private static String getFormatFromExtension(String extension) {
|
||||
switch (extension) {
|
||||
case "ttf":
|
||||
return "truetype";
|
||||
case "woff":
|
||||
return "woff";
|
||||
case "woff2":
|
||||
return "woff2";
|
||||
case "eot":
|
||||
return "embedded-opentype";
|
||||
case "svg":
|
||||
return "svg";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -8,18 +8,17 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -28,9 +27,7 @@ import stirling.software.common.util.EmlToPdf;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertEmlToPDF {
|
||||
@@ -40,7 +37,8 @@ public class ConvertEmlToPDF {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/eml/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/eml/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert EML to PDF",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -3,25 +3,22 @@ package stirling.software.SPDF.controller.api.converters;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.*;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertHtmlToPDF {
|
||||
|
||||
@@ -33,7 +30,8 @@ public class ConvertHtmlToPDF {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/html/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/html/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||
description =
|
||||
|
||||
+14
-13
@@ -22,13 +22,9 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -38,8 +34,12 @@ import stirling.software.SPDF.model.api.converters.ConvertCbrToPdfRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertCbzToPdfRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToCbrRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToCbzRequest;
|
||||
import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CbrUtils;
|
||||
import stirling.software.common.util.CbzUtils;
|
||||
@@ -55,10 +55,8 @@ import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertImgPDFController {
|
||||
|
||||
@@ -73,7 +71,9 @@ public class ConvertImgPDFController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img")
|
||||
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to image(s)",
|
||||
description =
|
||||
@@ -234,7 +234,8 @@ public class ConvertImgPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/img/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/img/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert images to a PDF file",
|
||||
description =
|
||||
@@ -262,7 +263,7 @@ public class ConvertImgPDFController {
|
||||
GeneralUtils.generateFilename(file[0].getOriginalFilename(), "_converted.pdf"));
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbz/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbz/pdf")
|
||||
@Operation(
|
||||
summary = "Convert CBZ comic book archive to PDF",
|
||||
description =
|
||||
@@ -298,7 +299,7 @@ public class ConvertImgPDFController {
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, filename);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbz")
|
||||
@Operation(
|
||||
summary = "Convert PDF to CBZ comic book archive",
|
||||
description =
|
||||
@@ -331,7 +332,7 @@ public class ConvertImgPDFController {
|
||||
cbzBytes, filename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/cbr/pdf")
|
||||
@Operation(
|
||||
summary = "Convert CBR comic book archive to PDF",
|
||||
description =
|
||||
@@ -367,7 +368,7 @@ public class ConvertImgPDFController {
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, filename);
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbr")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/cbr")
|
||||
@Operation(
|
||||
summary = "Convert PDF to CBR comic book archive",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -13,25 +13,22 @@ import org.commonmark.renderer.html.HtmlRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.*;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertMarkdownToPdf {
|
||||
|
||||
@@ -42,7 +39,8 @@ public class ConvertMarkdownToPdf {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/markdown/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/markdown/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a Markdown file to PDF",
|
||||
description =
|
||||
|
||||
+7
-8
@@ -15,19 +15,20 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
<<<<<<< HEAD
|
||||
import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
=======
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -38,9 +39,7 @@ import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ConvertOfficeController {
|
||||
@@ -176,7 +175,7 @@ public class ConvertOfficeController {
|
||||
.matches();
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/file/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/file/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a file to a PDF using LibreOffice",
|
||||
description =
|
||||
|
||||
+10
-9
@@ -3,33 +3,34 @@ package stirling.software.SPDF.controller.api.converters;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
<<<<<<< HEAD
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
=======
|
||||
import stirling.software.SPDF.config.swagger.HtmlConversionResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
>>>>>>> refs/remotes/origin/V2
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@RequiredArgsConstructor
|
||||
@ConvertApi
|
||||
public class ConvertPDFToHtml {
|
||||
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
|
||||
@Operation(
|
||||
summary = "Convert PDF to HTML",
|
||||
description =
|
||||
"This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO")
|
||||
@HtmlConversionResponse
|
||||
public ResponseEntity<byte[]> processPdfToHTML(@ModelAttribute PDFFile file) throws Exception {
|
||||
MultipartFile inputFile = file.getFileInput();
|
||||
PDFToFile pdfToFile = new PDFToFile(tempFileManager);
|
||||
|
||||
+15
-11
@@ -7,19 +7,21 @@ import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.PowerPointConversionResponse;
|
||||
import stirling.software.SPDF.config.swagger.TextPlainConversionResponse;
|
||||
import stirling.software.SPDF.config.swagger.WordConversionResponse;
|
||||
import stirling.software.SPDF.config.swagger.XmlConversionResponse;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -27,16 +29,15 @@ import stirling.software.common.util.PDFToFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertPDFToOffice {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
|
||||
@PowerPointConversionResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to Presentation format",
|
||||
description =
|
||||
@@ -51,7 +52,8 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
|
||||
@TextPlainConversionResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to Text or RTF format",
|
||||
description =
|
||||
@@ -77,7 +79,8 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
|
||||
@WordConversionResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to Word document",
|
||||
description =
|
||||
@@ -91,7 +94,8 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
|
||||
@XmlConversionResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to XML",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -57,30 +57,28 @@ import org.apache.xmpbox.xml.XmpSerializer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ConvertPDFToPDFA {
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A",
|
||||
description =
|
||||
|
||||
+6
-5
@@ -19,12 +19,14 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -33,10 +35,8 @@ import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@RequiredArgsConstructor
|
||||
public class ConvertWebsiteToPDF {
|
||||
|
||||
@@ -44,7 +44,8 @@ public class ConvertWebsiteToPDF {
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description =
|
||||
|
||||
+6
-8
@@ -18,18 +18,17 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.CsvConversionResponse;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@@ -38,16 +37,15 @@ import technology.tabula.Page;
|
||||
import technology.tabula.Table;
|
||||
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
@ConvertApi
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ExtractCSVController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(value = "/pdf/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@AutoJobPostMapping(value = "/pdf/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@CsvConversionResponse
|
||||
@Operation(
|
||||
summary = "Extracts a CSV document from a PDF",
|
||||
description =
|
||||
|
||||
+16
-13
@@ -8,37 +8,35 @@ import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.FilterResponse;
|
||||
import stirling.software.SPDF.model.api.PDFComparisonAndCount;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
|
||||
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.FilterApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/filter")
|
||||
@Tag(name = "Filter", description = "Filter APIs")
|
||||
@FilterApi
|
||||
@RequiredArgsConstructor
|
||||
public class FilterController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-text")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-text")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains set text, returns true if does",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -58,7 +56,8 @@ public class FilterController {
|
||||
}
|
||||
|
||||
// TODO
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-contains-image")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains an image",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -74,7 +73,8 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-count")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -101,7 +101,8 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -139,7 +140,8 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is a set file size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -167,7 +169,8 @@ public class FilterController {
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-rotation")
|
||||
@FilterResponse
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain rotation",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
|
||||
+6
-8
@@ -7,36 +7,34 @@ import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
public class AttachmentController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
private final AttachmentServiceInterface pdfAttachmentService;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add attachments to PDF",
|
||||
description =
|
||||
|
||||
+4
-8
@@ -11,27 +11,23 @@ import org.apache.pdfbox.text.TextPosition;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@MiscApi
|
||||
@Slf4j
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@RequiredArgsConstructor
|
||||
public class AutoRenameController {
|
||||
|
||||
@@ -40,7 +36,7 @@ public class AutoRenameController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename")
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename")
|
||||
@Operation(
|
||||
summary = "Extract header from PDF file",
|
||||
description =
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user