Compare commits
11 Commits
mac
...
bug/rememb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2062c19ab | ||
|
|
6760fddd6f | ||
|
|
20a75d577d | ||
|
|
559581c59d | ||
|
|
e7356a1d38 | ||
|
|
1405e4f5ee | ||
|
|
322e7dee0d | ||
|
|
918e977c6a | ||
|
|
a31633e5b8 | ||
|
|
ed551cec91 | ||
|
|
3f14e77725 |
117
.github/scripts/check_language_properties.py
vendored
117
.github/scripts/check_language_properties.py
vendored
@@ -9,9 +9,8 @@ The script also provides functionality to update the translation files to match
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python check_language_properties.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
python script_name.py --reference-file <path_to_reference_file> --branch <branch_name> [--files <list_of_changed_files>]
|
||||
"""
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
@@ -19,10 +18,6 @@ import argparse
|
||||
import re
|
||||
|
||||
|
||||
# Maximum size for properties files (e.g., 200 KB)
|
||||
MAX_FILE_SIZE = 200 * 1024
|
||||
|
||||
|
||||
def parse_properties_file(file_path):
|
||||
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
|
||||
properties_list = []
|
||||
@@ -100,7 +95,7 @@ def write_json_file(file_path, updated_properties):
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
reference_properties = parse_properties_file(reference_file)
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
basename_current_file = os.path.basename(branch + file_path)
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".properties")
|
||||
@@ -108,7 +103,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_properties_file(os.path.join(branch, file_path))
|
||||
current_properties = parse_properties_file(branch + file_path)
|
||||
updated_properties = []
|
||||
for ref_entry in reference_properties:
|
||||
ref_entry_copy = copy.deepcopy(ref_entry)
|
||||
@@ -119,79 +114,60 @@ def update_missing_keys(reference_file, file_list, branch=""):
|
||||
if ref_entry_copy["key"] == current_entry["key"]:
|
||||
ref_entry_copy["value"] = current_entry["value"]
|
||||
updated_properties.append(ref_entry_copy)
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
write_json_file(branch + file_path, updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
update_missing_keys(reference_file, file_list, branch + "/")
|
||||
|
||||
|
||||
def read_properties(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.read().splitlines()
|
||||
return [""]
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.read().splitlines()
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
def check_for_differences(reference_file, file_list, branch):
|
||||
reference_branch = reference_file.split("/")[0]
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
report.append(
|
||||
f"### 📋 Checking with the file `{basename_reference_file}` from the `{reference_branch}` - Checking the `{branch}`"
|
||||
)
|
||||
reference_lines = read_properties(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
|
||||
file_arr = file_list
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
|
||||
|
||||
for file_path in file_arr:
|
||||
absolute_path = os.path.abspath(file_path)
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.startswith(base_dir):
|
||||
raise ValueError(f"Unsafe file found: {file_path}")
|
||||
# Verify file size before processing
|
||||
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"The file {file_path} is too large and could pose a security risk."
|
||||
)
|
||||
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(branch + "/" + file_path)
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
or not file_path.startswith(
|
||||
os.path.join("src", "main", "resources", "messages_")
|
||||
)
|
||||
or not file_path.endswith(".properties")
|
||||
or not basename_current_file.startswith("messages_")
|
||||
):
|
||||
continue
|
||||
only_reference_file = False
|
||||
report.append(f"#### 📃 **File Check:** `{basename_current_file}`")
|
||||
current_lines = read_properties(os.path.join(branch, file_path))
|
||||
report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
|
||||
current_lines = read_properties(branch + "/" + file_path)
|
||||
reference_line_count = len(reference_lines)
|
||||
current_line_count = len(current_lines)
|
||||
|
||||
if reference_line_count != current_line_count:
|
||||
report.append("")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
report.append("- **Test 1 Status:** ❌ Failed")
|
||||
has_differences = True
|
||||
if reference_line_count > current_line_count:
|
||||
report.append(
|
||||
f" - **_Mismatched line count_**: {reference_line_count} (reference) vs {current_line_count} (current). Comments, empty lines, or translation strings are missing."
|
||||
f" - **Issue:** Missing lines! Comments, empty lines, or translation strings are missing. Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
||||
)
|
||||
elif reference_line_count < current_line_count:
|
||||
report.append(
|
||||
f" - **_Too many lines_**: {reference_line_count} (reference) vs {current_line_count} (current). Please verify if there is an additional line that needs to be removed."
|
||||
f" - **Issue:** Too many lines! Check your translation files! Details: {reference_line_count} (reference) vs {current_line_count} (current)."
|
||||
)
|
||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
||||
else:
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
report.append("- **Test 1 Status:** ✅ Passed")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys = []
|
||||
@@ -216,42 +192,32 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
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:**")
|
||||
report.append("- **Test 2 Status:** ❌ Failed")
|
||||
if missing_keys_list:
|
||||
spaces_keys_list = []
|
||||
for key in missing_keys_list:
|
||||
if " " in key:
|
||||
spaces_keys_list.append(key)
|
||||
if spaces_keys_list:
|
||||
spaces_keys_str = "`, `".join(spaces_keys_list)
|
||||
report.append(
|
||||
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
|
||||
)
|
||||
report.append(
|
||||
f" - **_Extra keys in `{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
f" - **Issue:** There are keys in ***{basename_current_file}*** `{missing_keys_str}` that are not present in ***{basename_reference_file}***!"
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{basename_reference_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_current_file}`_**."
|
||||
f" - **Issue:** There are keys in ***{basename_reference_file}*** `{extra_keys_str}` that are not present in ***{basename_current_file}***!"
|
||||
)
|
||||
# update_missing_keys(reference_file, [file_path], branch + "/")
|
||||
else:
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
report.append("- **Test 2 Status:** ✅ Passed")
|
||||
# if has_differences:
|
||||
# report.append("")
|
||||
# report.append(f"#### 🚧 ***{basename_current_file}*** will be corrected...")
|
||||
report.append("")
|
||||
report.append("---")
|
||||
report.append("")
|
||||
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
|
||||
# update_missing_keys(reference_file, update_file_list)
|
||||
# report.append("---")
|
||||
# report.append("")
|
||||
if has_differences:
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
@@ -259,11 +225,6 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
||||
|
||||
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,
|
||||
@@ -283,21 +244,11 @@ if __name__ == "__main__":
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Sanitize --actor input to avoid injection attacks
|
||||
if args.actor:
|
||||
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||
|
||||
# Sanitize --branch input to avoid injection attacks
|
||||
if args.branch:
|
||||
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||
|
||||
file_list = args.files
|
||||
if file_list is None:
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(), "src", "main", "resources", "messages_*.properties"
|
||||
)
|
||||
os.getcwd() + "/src/**/messages_*.properties", recursive=True
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
check_for_differences(args.reference_file, file_list, args.branch)
|
||||
|
||||
179
.github/workflows/PR-Demo-Comment.yml
vendored
179
.github/workflows/PR-Demo-Comment.yml
vendored
@@ -1,179 +0,0 @@
|
||||
name: PR Deployment via Comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
check-comment:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
github.event.issue.pull_request &&
|
||||
(
|
||||
contains(github.event.comment.body, 'prdeploy') ||
|
||||
contains(github.event.comment.body, 'deploypr')
|
||||
)
|
||||
&&
|
||||
(
|
||||
github.event.comment.user.login == 'frooodle' ||
|
||||
github.event.comment.user.login == 'sf298' ||
|
||||
github.event.comment.user.login == 'Ludy87' ||
|
||||
github.event.comment.user.login == 'LaserKaspar' ||
|
||||
github.event.comment.user.login == 'sbplat' ||
|
||||
github.event.comment.user.login == 'reecebrowne'
|
||||
)
|
||||
outputs:
|
||||
pr_number: ${{ steps.get-pr.outputs.pr_number }}
|
||||
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
|
||||
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
|
||||
|
||||
steps:
|
||||
- name: Get PR data
|
||||
id: get-pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const prNumber = context.payload.issue.number;
|
||||
console.log(`PR Number: ${prNumber}`);
|
||||
core.setOutput('pr_number', prNumber);
|
||||
|
||||
- name: Get PR repository and ref
|
||||
id: get-pr-info
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const prNumber = context.payload.issue.number;
|
||||
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: prNumber,
|
||||
});
|
||||
|
||||
// For forks, use the full repository name, for internal PRs use the current repo
|
||||
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
|
||||
|
||||
console.log(`PR Repository: ${repository}`);
|
||||
console.log(`PR Branch: ${pr.head.ref}`);
|
||||
|
||||
core.setOutput('repository', repository);
|
||||
core.setOutput('ref', pr.head.ref);
|
||||
|
||||
deploy-pr:
|
||||
needs: check-comment
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ needs.check-comment.outputs.pr_repository }}
|
||||
ref: ${{ needs.check-comment.outputs.pr_ref }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Run Gradle Command
|
||||
run: ./gradlew clean build
|
||||
env:
|
||||
DOCKER_ENABLE_SECURITY: false
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Get version number
|
||||
id: versionNumber
|
||||
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_API }}
|
||||
|
||||
- name: Build and push PR-specific image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Deploy to VPS
|
||||
run: |
|
||||
# First create the docker-compose content locally
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
|
||||
ports:
|
||||
- "${{ needs.check-comment.outputs.pr_number }}:8080"
|
||||
volumes:
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
|
||||
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
|
||||
environment:
|
||||
DOCKER_ENABLE_SECURITY: "false"
|
||||
SECURITY_ENABLELOGIN: "false"
|
||||
SYSTEM_DEFAULTLOCALE: en-GB
|
||||
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
|
||||
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
|
||||
SYSTEM_MAXFILESIZE: "100"
|
||||
METRICS_ENABLED: "true"
|
||||
SYSTEM_GOOGLEVISIBILITY: "false"
|
||||
restart: on-failure:5
|
||||
EOF
|
||||
|
||||
# Then copy the file and execute commands
|
||||
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
|
||||
|
||||
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
# Create PR-specific directories
|
||||
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
|
||||
|
||||
# Move docker-compose file to correct location
|
||||
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
|
||||
|
||||
# Start or restart the container
|
||||
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
ENDSSH
|
||||
|
||||
- name: Post deployment URL to PR
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
|
||||
|
||||
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
|
||||
const commentBody = `## 🚀 PR Test Deployment\n\n` +
|
||||
`Your PR has been deployed for testing!\n\n` +
|
||||
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
|
||||
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
78
.github/workflows/PR-Demo-cleanup.yml
vendored
78
.github/workflows/PR-Demo-cleanup.yml
vendored
@@ -1,78 +0,0 @@
|
||||
name: PR Deployment cleanup
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
|
||||
CLEANUP_PERFORMED: 'false' # Add flag to track if cleanup occurred
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action == 'closed'
|
||||
|
||||
steps:
|
||||
- name: Set up SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh/
|
||||
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
|
||||
sudo chmod 600 ../private.key
|
||||
|
||||
- name: Cleanup PR deployment
|
||||
id: cleanup
|
||||
run: |
|
||||
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
|
||||
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
|
||||
echo "Found PR directory, proceeding with cleanup..."
|
||||
|
||||
# Stop and remove containers
|
||||
cd /stirling/PR-${{ github.event.pull_request.number }}
|
||||
docker-compose down || true
|
||||
|
||||
# Go back to root before removal
|
||||
cd /
|
||||
|
||||
# Remove PR-specific directories
|
||||
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
|
||||
|
||||
# Remove the Docker image
|
||||
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
|
||||
|
||||
echo "PERFORMED_CLEANUP"
|
||||
else
|
||||
echo "PR directory not found, nothing to clean up"
|
||||
echo "NO_CLEANUP_NEEDED"
|
||||
fi
|
||||
ENDSSH
|
||||
)
|
||||
|
||||
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
|
||||
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cleanup_performed=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Post cleanup notice to PR
|
||||
if: steps.cleanup.outputs.cleanup_performed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
const commentBody = `## 🧹 Deployment Cleanup\n\n` +
|
||||
`The test deployment for this PR has been cleaned up.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: commentBody
|
||||
});
|
||||
119
.github/workflows/check_properties.yml
vendored
119
.github/workflows/check_properties.yml
vendored
@@ -6,22 +6,18 @@ on:
|
||||
paths:
|
||||
- "src/main/resources/messages_*.properties"
|
||||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/main/resources/messages_en_GB.properties"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
check-files:
|
||||
if: github.event_name == 'pull_request_target'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout main branch first
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout PR branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -30,6 +26,13 @@ jobs:
|
||||
path: pr-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Checkout main branch
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
path: main-branch
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -46,73 +49,56 @@ jobs:
|
||||
echo "Fetching PR changed files..."
|
||||
cd pr-branch
|
||||
gh repo set-default ${{ github.repository }}
|
||||
# Store files in a safe way, only allowing valid properties files
|
||||
echo "Getting list of changed files from PR..."
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
|
||||
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
|
||||
cd ..
|
||||
|
||||
echo "Processing changed files..."
|
||||
mapfile -t CHANGED_FILES < changed_files.txt
|
||||
|
||||
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
|
||||
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
|
||||
|
||||
echo "Changed files: ${CHANGED_FILES_STR}"
|
||||
echo $(cat changed_files.txt)
|
||||
BRANCH_PATH="pr-branch"
|
||||
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
|
||||
CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
|
||||
echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
|
||||
echo "Changed files: ${CHANGED_FILES}"
|
||||
echo "Branch: ${BRANCH_PATH}"
|
||||
|
||||
- name: Determine reference file
|
||||
id: determine-file
|
||||
run: |
|
||||
echo "Determining reference file..."
|
||||
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then
|
||||
echo "Using PR branch reference file"
|
||||
if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
|
||||
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using main branch reference file"
|
||||
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
|
||||
fi
|
||||
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
|
||||
|
||||
- name: Show REFERENCE_FILE
|
||||
run: echo "Reference file is set to ${REFERENCE_FILE}"
|
||||
run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
|
||||
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
echo "Running Python script to check files..."
|
||||
python main-branch/.github/scripts/check_language_properties.py \
|
||||
--actor ${{ github.event.pull_request.user.login }} \
|
||||
--reference-file "${REFERENCE_FILE}" \
|
||||
--branch pr-branch \
|
||||
--files "${CHANGED_FILES[@]}" > result.txt || true
|
||||
python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
|
||||
|
||||
- name: Capture output
|
||||
id: capture-output
|
||||
run: |
|
||||
if [ -f result.txt ] && [ -s result.txt ]; then
|
||||
echo "Test, capturing output..."
|
||||
SCRIPT_OUTPUT=$(cat result.txt)
|
||||
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV
|
||||
if [ -f failure.txt ] && [ -s failure.txt ]; then
|
||||
echo "Test failed, capturing output..."
|
||||
ERROR_OUTPUT=$(cat failure.txt)
|
||||
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
|
||||
echo "$ERROR_OUTPUT" >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo "${SCRIPT_OUTPUT}"
|
||||
|
||||
# Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
|
||||
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
|
||||
echo "FAIL_JOB=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
fi
|
||||
echo $ERROR_OUTPUT
|
||||
else
|
||||
echo "No update found."
|
||||
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV
|
||||
echo "FAIL_JOB=false" >> $GITHUB_ENV
|
||||
echo "No errors found."
|
||||
echo "ERROR_OUTPUT=" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Post comment on PR
|
||||
if: env.SCRIPT_OUTPUT != ''
|
||||
if: env.ERROR_OUTPUT != ''
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env;
|
||||
const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
|
||||
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
|
||||
const prNumber = context.issue.number;
|
||||
|
||||
@@ -134,7 +120,7 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
comment_id: comment.id,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
||||
});
|
||||
console.log("Updated existing comment.");
|
||||
} else if (!comment) {
|
||||
@@ -143,24 +129,33 @@ jobs:
|
||||
owner: repoOwner,
|
||||
repo: repoName,
|
||||
issue_number: prNumber,
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n`
|
||||
body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
|
||||
});
|
||||
console.log("Created new comment.");
|
||||
} else {
|
||||
console.log("Comment update attempt denied. Actor does not match.");
|
||||
}
|
||||
|
||||
- name: Fail job if errors found
|
||||
if: env.FAIL_JOB == 'true'
|
||||
run: |
|
||||
echo "Failing the job because errors were detected."
|
||||
exit 1
|
||||
# - name: Set up git config
|
||||
# run: |
|
||||
# git config --global user.name "github-actions[bot]"
|
||||
# git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# - name: Add translation keys
|
||||
# run: |
|
||||
# cd ${{ env.BRANCH_PATH }}
|
||||
# git add src/main/resources/messages_*.properties
|
||||
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
|
||||
# git commit -m "Update translation files" || echo "No changes to commit"
|
||||
# - name: Push
|
||||
# if: env.CHANGES_DETECTED == 'true'
|
||||
# run: |
|
||||
# cd pr-branch
|
||||
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
|
||||
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
|
||||
|
||||
update-translations-main:
|
||||
if: github.event_name == 'push'
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -174,10 +169,7 @@ jobs:
|
||||
- name: Run Python script to check files
|
||||
id: run-check
|
||||
run: |
|
||||
echo "Running Python script to check files..."
|
||||
python .github/scripts/check_language_properties.py \
|
||||
--reference-file src/main/resources/messages_en_GB.properties \
|
||||
--branch main
|
||||
python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
|
||||
|
||||
- name: Set up git config
|
||||
run: |
|
||||
@@ -192,7 +184,7 @@ jobs:
|
||||
- name: Create Pull Request
|
||||
id: cpr
|
||||
if: env.CHANGES_DETECTED == 'true'
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "Update translation files"
|
||||
@@ -201,8 +193,6 @@ jobs:
|
||||
signoff: true
|
||||
branch: update_translation_files
|
||||
title: "Update translation files"
|
||||
add-paths: |
|
||||
src/main/resources/messages_*.properties
|
||||
body: |
|
||||
Auto-generated by [create-pull-request][1]
|
||||
|
||||
@@ -210,4 +200,3 @@ jobs:
|
||||
labels: Translation
|
||||
draft: false
|
||||
delete-branch: true
|
||||
sign-commits: true
|
||||
|
||||
67
.github/workflows/mac-unix-artifact-creation.yml
vendored
67
.github/workflows/mac-unix-artifact-creation.yml
vendored
@@ -1,67 +0,0 @@
|
||||
name: Create Application Bundles
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'mac'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-unix-bundle:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew bootJar
|
||||
|
||||
- name: Create tar.gz Bundle
|
||||
run: |
|
||||
mkdir -p Stirling-PDF-unix
|
||||
cp build/libs/Stirling-PDF-*.jar Stirling-PDF-unix/Stirling-PDF.jar
|
||||
cp scripts/launcher.sh Stirling-PDF-unix/Stirling-PDF
|
||||
cp src/main/resources/static/favicon.ico Stirling-PDF-unix/icon.png
|
||||
chmod +x Stirling-PDF-unix/Stirling-PDF
|
||||
tar -czf Stirling-PDF-unix.tar.gz Stirling-PDF-unix/
|
||||
|
||||
- name: Upload Unix Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unix-bundle
|
||||
path: Stirling-PDF-unix.tar.gz
|
||||
|
||||
create-mac-bundle:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: '21'
|
||||
distribution: 'temurin'
|
||||
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew bootJar
|
||||
|
||||
- name: Create Mac App Bundle
|
||||
run: |
|
||||
cp build/libs/Stirling-PDF-*.jar build/libs/Stirling-PDF.jar
|
||||
chmod +x scripts/create-mac-launcher.sh
|
||||
./scripts/create-mac-launcher.sh
|
||||
|
||||
- name: Create DMG
|
||||
run: |
|
||||
hdiutil create -volname "Stirling-PDF" -srcfolder "Stirling-PDF.app" -ov -format UDZO "Stirling-PDF-mac.dmg"
|
||||
|
||||
- name: Upload Mac Bundle
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mac-bundle
|
||||
path: Stirling-PDF-mac.dmg
|
||||
3
.github/workflows/push-docker.yml
vendored
3
.github/workflows/push-docker.yml
vendored
@@ -67,7 +67,6 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -96,7 +95,6 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
@@ -124,7 +122,6 @@ jobs:
|
||||
images: |
|
||||
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
|
||||
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
|
||||
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
|
||||
tags: |
|
||||
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
@@ -11,7 +11,7 @@ Stirling-PDF is built using:
|
||||
- Spring Boot + Thymeleaf
|
||||
- PDFBox
|
||||
- LibreOffice
|
||||
- qpdf
|
||||
- OcrMyPdf
|
||||
- HTML, CSS, JavaScript
|
||||
- Docker
|
||||
- PDF.js
|
||||
@@ -114,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
@@ -173,20 +173,20 @@ Stirling-PDF uses different Docker images for various configurations. The build
|
||||
For the latest version:
|
||||
|
||||
```bash
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-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 .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
|
||||
```
|
||||
|
||||
For the fat version (with security enabled):
|
||||
|
||||
```bash
|
||||
export DOCKER_ENABLE_SECURITY=true
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
|
||||
```
|
||||
|
||||
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
|
||||
@@ -243,7 +243,7 @@ To run Stirling-PDF locally:
|
||||
|
||||
Important notes:
|
||||
|
||||
- Local testing doesn't include features that depend on external tools like qpdf, LibreOffice, or Python scripts.
|
||||
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
|
||||
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
|
||||
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
tini \
|
||||
bash \
|
||||
curl \
|
||||
qpdf \
|
||||
shadow \
|
||||
su-exec \
|
||||
openssl \
|
||||
@@ -41,6 +40,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
# pdftohtml
|
||||
poppler-utils \
|
||||
# OCR MY PDF (unpaper for descew and other advanced features)
|
||||
ocrmypdf \
|
||||
tesseract-ocr-data-eng \
|
||||
# CV
|
||||
py3-opencv \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Build the application
|
||||
FROM gradle:8.11-jdk17 AS build
|
||||
FROM gradle:8.7-jdk17 AS build
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
@@ -55,7 +55,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
|
||||
# pdftohtml
|
||||
poppler-utils \
|
||||
# OCR MY PDF (unpaper for descew and other advanced featues)
|
||||
qpdf \
|
||||
ocrmypdf \
|
||||
tesseract-ocr-data-eng \
|
||||
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
|
||||
# CV
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | qpdf | Java | Javascript | Unoconv | tesseract |
|
||||
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript | Unoconv | Ghostscript |
|
||||
| ------------------- | ------- | ------- | -------- | ----- | --- | ------ | ------ | ----------- | -------- | ---- | ---------- | ------- | ----------- |
|
||||
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
|
||||
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
|
||||
@@ -16,7 +16,7 @@
|
||||
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
|
||||
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
|
||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | |
|
||||
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | ✔️ |
|
||||
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
|
||||
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
|
||||
@@ -34,13 +34,13 @@
|
||||
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| compare | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
||||
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | ✔️ |
|
||||
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||
| extract-images | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| flatten | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
|
||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | ✔ |
|
||||
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
|
||||
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
|
||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | ✔ | | | | |
|
||||
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | ✔️ |
|
||||
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
| sign | | | | ✔️ | | | | | | | ✔️ | | |
|
||||
|
||||
@@ -8,7 +8,7 @@ The paths have changed for the tessdata locations on new Docker images. Please u
|
||||
|
||||
## How does the OCR Work
|
||||
|
||||
Stirling-PDF uses Tesseract for its text recognition. All credit goes to them for this awesome work!
|
||||
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF), which in turn uses Tesseract for its text recognition. All credit goes to them for this awesome work!
|
||||
|
||||
## Language Packs
|
||||
|
||||
@@ -52,6 +52,8 @@ Add the following to your existing Docker run command:
|
||||
|
||||
### Non-Docker Setup
|
||||
|
||||
If you are not using Docker, you need to install the OCR components, including the `ocrmypdf` app. You can see the [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html).
|
||||
|
||||
For Debian-based systems, install languages with this command:
|
||||
|
||||
```bash
|
||||
@@ -81,7 +83,8 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
|
||||
|
||||
For Windows:
|
||||
|
||||
You must ensure tesseract is installed
|
||||
Ensure ocrmypdf in installed with
|
||||
``pip install ocrmypdf``
|
||||
|
||||
Additional languages must be downloaded manually:
|
||||
Download desired .traineddata files from tessdata or tessdata_fast
|
||||
|
||||
45
Jenkinsfile
vendored
Normal file
45
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
pipeline {
|
||||
agent any
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'chmod 755 gradlew'
|
||||
sh './gradlew build'
|
||||
}
|
||||
}
|
||||
stage('Docker Build') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
sh "docker build -t $image ."
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Docker Push') {
|
||||
steps {
|
||||
script {
|
||||
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
|
||||
def image = "frooodle/s-pdf:$appVersion"
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "docker push $image"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Helm Push') {
|
||||
steps {
|
||||
script {
|
||||
//TODO: Read chartVersion from Chart.yaml
|
||||
def chartVersion = '1.0.0'
|
||||
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
|
||||
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
|
||||
sh "helm package chart/stirling-pdf"
|
||||
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ nix-env -iA nixpkgs.jbig2enc
|
||||
|
||||
### Step 3: Install Additional Software
|
||||
|
||||
Next we need to install LibreOffice for conversions, qpdf for OCR, and OpenCV for pattern recognition functionality.
|
||||
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and OpenCV for pattern recognition functionality.
|
||||
|
||||
Install the following software:
|
||||
|
||||
@@ -81,27 +81,27 @@ Install the following software:
|
||||
- unoconv
|
||||
- pngquant
|
||||
- unpaper
|
||||
- qpdf
|
||||
- ocrmypdf
|
||||
- opencv-python-headless
|
||||
|
||||
For Debian-based systems, you can use the following command:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
||||
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
|
||||
```
|
||||
|
||||
For Fedora:
|
||||
|
||||
```bash
|
||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf
|
||||
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||
```
|
||||
|
||||
For Nix:
|
||||
|
||||
```bash
|
||||
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.qpdf nixpkgs.poppler_utils
|
||||
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
|
||||
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
|
||||
```
|
||||
|
||||
@@ -146,6 +146,7 @@ The easiest method is to use the language packs provided by your repositories. S
|
||||
|
||||
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
|
||||
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
|
||||
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
|
||||
|
||||
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
|
||||
|
||||
|
||||
97
README.md
97
README.md
@@ -6,7 +6,6 @@
|
||||
[](https://github.com/Stirling-Tools/Stirling-PDF/)
|
||||
[](https://github.com/Stirling-Tools/stirling-pdf)
|
||||
|
||||
<a href="https://www.producthunt.com/posts/stirling-pdf?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-stirling-pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling PDF - Open source locally hosted web PDF editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
[](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
|
||||
|
||||
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
|
||||
@@ -19,7 +18,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
||||
|
||||
## Features
|
||||
|
||||
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
|
||||
- Dark mode support
|
||||
- Custom download options
|
||||
- Parallel file processing and downloads
|
||||
@@ -79,15 +78,15 @@ All files and PDFs exist either exclusively on the client side, reside in server
|
||||
- Detect and remove blank pages
|
||||
- Compare two PDFs and show differences in text
|
||||
- Add images to PDFs
|
||||
- Compress PDFs to decrease their filesize (using qpdf)
|
||||
- Compress PDFs to decrease their filesize (using OCRMyPDF)
|
||||
- Extract images from PDF
|
||||
- Remove images from PDF
|
||||
- Extract images from scans
|
||||
- Remove annotations
|
||||
- Add page numbers
|
||||
- Auto rename file by detecting PDF header text
|
||||
- OCR on PDF (using tesseract)
|
||||
- PDF/A conversion (using libreoffice)
|
||||
- OCR on PDF (using OCRMyPDF)
|
||||
- PDF/A conversion (using OCRMyPDF)
|
||||
- Edit metadata
|
||||
- Flatten PDFs
|
||||
- Get all information on a PDF to view or export as JSON
|
||||
@@ -102,7 +101,7 @@ A demo of the app is available [here](https://stirlingpdf.io).
|
||||
- Spring Boot + Thymeleaf
|
||||
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
|
||||
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
|
||||
- [qpdf](https://github.com/qpdf/qpdf)
|
||||
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
|
||||
- HTML, CSS, JavaScript
|
||||
- Docker
|
||||
- [PDF.js](https://github.com/mozilla/pdf.js)
|
||||
@@ -121,13 +120,13 @@ Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/b
|
||||
### Docker / Podman
|
||||
|
||||
> [!NOTE]
|
||||
> <https://hub.docker.com/r/stirlingtools/stirling-pdf>
|
||||
> <https://hub.docker.com/r/frooodle/s-pdf>
|
||||
|
||||
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
|
||||
|
||||
@@ -145,7 +144,7 @@ docker run -d \
|
||||
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
|
||||
-e LANGS=en_GB \
|
||||
--name stirling-pdf \
|
||||
stirlingtools/stirling-pdf:latest
|
||||
frooodle/s-pdf:latest
|
||||
```
|
||||
|
||||
### Docker Compose
|
||||
@@ -154,7 +153,7 @@ docker run -d \
|
||||
version: '3.3'
|
||||
services:
|
||||
stirling-pdf:
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
image: frooodle/s-pdf:latest
|
||||
ports:
|
||||
- '8080:8080'
|
||||
volumes:
|
||||
@@ -187,48 +186,46 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
|
||||
|
||||
## Supported Languages
|
||||
|
||||
Stirling-PDF currently supports 38 languages!
|
||||
Stirling-PDF currently supports 36 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) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
|
||||
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
|
||||
|
||||
@@ -241,7 +238,7 @@ Stirling PDF offers a Enterprise edition of its software, This is the same great
|
||||
### Whats included
|
||||
|
||||
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
|
||||
- Prioritised Enhancements to Stirling-PDF where applicable
|
||||
- Prioritised Enhancements to Stirling-PDF where applicable
|
||||
- Base SSO support
|
||||
- Advanced SSO such as automated login handling (Coming very soon)
|
||||
- SAML SSO (Coming very soon)
|
||||
|
||||
@@ -8,7 +8,7 @@ The 'Fat' container contains all those found in 'Full' with security jar along w
|
||||
| Libre | | ✔️ |
|
||||
| Python | | ✔️ |
|
||||
| OpenCV | | ✔️ |
|
||||
| qpdf | | ✔️ |
|
||||
| OCRmyPDF | | ✔️ |
|
||||
|
||||
| Operation | Ultra-Lite | Full |
|
||||
| ---------------------- | ---------- | ---- |
|
||||
|
||||
46
build.gradle
46
build.gradle
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id "java"
|
||||
id "org.springframework.boot" version "3.4.0"
|
||||
id "org.springframework.boot" version "3.3.5"
|
||||
id "io.spring.dependency-management" version "1.1.6"
|
||||
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
|
||||
id "io.swagger.swaggerhub" version "1.3.2"
|
||||
@@ -10,24 +10,19 @@ plugins {
|
||||
//id "nebula.lint" version "19.0.3"
|
||||
}
|
||||
|
||||
|
||||
|
||||
import com.github.jk1.license.render.*
|
||||
|
||||
ext {
|
||||
springBootVersion = "3.4.0"
|
||||
springBootVersion = "3.3.5"
|
||||
pdfboxVersion = "3.0.3"
|
||||
logbackVersion = "1.5.7"
|
||||
imageioVersion = "3.12.0"
|
||||
lombokVersion = "1.18.36"
|
||||
bouncycastleVersion = "1.79"
|
||||
springSecuritySamlVersion = "6.4.1"
|
||||
openSamlVersion = "4.3.2"
|
||||
lombokVersion = "1.18.34"
|
||||
bouncycastleVersion = "1.78.1"
|
||||
}
|
||||
|
||||
group = "stirling.software"
|
||||
version = "0.36.0"
|
||||
|
||||
version = "0.32.0"
|
||||
|
||||
java {
|
||||
// 17 is lowest but we support and recommend 21
|
||||
@@ -124,7 +119,7 @@ configurations.all {
|
||||
}
|
||||
dependencies {
|
||||
//security updates
|
||||
implementation "org.springframework:spring-webmvc:6.2.0"
|
||||
implementation "org.springframework:spring-webmvc:6.1.14"
|
||||
|
||||
implementation("io.github.pixee:java-security-toolkit:1.2.0")
|
||||
|
||||
@@ -146,18 +141,18 @@ dependencies {
|
||||
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
|
||||
|
||||
implementation "org.springframework.session:spring-session-core:$springBootVersion"
|
||||
|
||||
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.3.4'
|
||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||
// Don't upgrade h2database
|
||||
runtimeOnly "com.h2database:h2:2.3.232"
|
||||
//2.2.x requires rebuild of DB file.. need migration path
|
||||
runtimeOnly "com.h2database:h2:2.1.214"
|
||||
// implementation "com.h2database:h2:2.2.224"
|
||||
constraints {
|
||||
implementation "org.opensaml:opensaml-core:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
|
||||
implementation "org.opensaml:opensaml-core"
|
||||
implementation "org.opensaml:opensaml-saml-api"
|
||||
implementation "org.opensaml:opensaml-saml-impl"
|
||||
}
|
||||
implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
|
||||
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
|
||||
implementation "org.springframework.security:spring-security-saml2-service-provider"
|
||||
|
||||
implementation 'com.coveo:saml-client:5.0.0'
|
||||
|
||||
|
||||
@@ -189,7 +184,7 @@ dependencies {
|
||||
// Image metadata extractor
|
||||
implementation "com.drewnoakes:metadata-extractor:2.19.0"
|
||||
|
||||
implementation "commons-io:commons-io:2.18.0"
|
||||
implementation "commons-io:commons-io:2.17.0"
|
||||
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
|
||||
//general PDF
|
||||
|
||||
@@ -206,19 +201,12 @@ dependencies {
|
||||
exclude group: "commons-logging", module: "commons-logging"
|
||||
}
|
||||
|
||||
// https://mvnrepository.com/artifact/technology.tabula/tabula
|
||||
implementation ('technology.tabula:tabula:1.0.5') {
|
||||
exclude group: "org.slf4j", module: "slf4j-simple"
|
||||
exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
|
||||
exclude group: "com.google.code.gson", module: "gson"
|
||||
}
|
||||
|
||||
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
|
||||
|
||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
implementation "io.micrometer:micrometer-core:1.14.1"
|
||||
implementation "io.micrometer:micrometer-core:1.13.6"
|
||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||
implementation "org.commonmark:commonmark:0.24.0"
|
||||
|
||||
@@ -48,6 +48,24 @@ Feature: API Validation
|
||||
And the response status code should be 200
|
||||
|
||||
|
||||
|
||||
@ocr @negative
|
||||
Scenario: Process PDF with text and OCR with type normal
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the pdf contains 3 pages with random text
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| languages | eng |
|
||||
| sidecar | false |
|
||||
| deskew | true |
|
||||
| clean | true |
|
||||
| cleanFinal | true |
|
||||
| ocrType | Normal |
|
||||
| ocrRenderType | hocr |
|
||||
| removeImagesAfter| false |
|
||||
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||
Then the response status code should be 500
|
||||
|
||||
@ocr @positive
|
||||
Scenario: Process PDF with OCR
|
||||
Given I generate a PDF file as "fileInput"
|
||||
@@ -65,6 +83,26 @@ Feature: API Validation
|
||||
Then the response content type should be "application/pdf"
|
||||
And the response file should have size greater than 0
|
||||
And the response status code should be 200
|
||||
|
||||
@ocr @positive
|
||||
Scenario: Process PDF with OCR with sidecar
|
||||
Given I generate a PDF file as "fileInput"
|
||||
And the request data includes
|
||||
| parameter | value |
|
||||
| languages | eng |
|
||||
| sidecar | true |
|
||||
| deskew | true |
|
||||
| clean | true |
|
||||
| cleanFinal | true |
|
||||
| ocrType | Force |
|
||||
| ocrRenderType | hocr |
|
||||
| removeImagesAfter| false |
|
||||
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
|
||||
Then the response content type should be "application/octet-stream"
|
||||
And the response file should have extension ".zip"
|
||||
And the response ZIP should contain 2 files
|
||||
And the response file should have size greater than 0
|
||||
And the response status code should be 200
|
||||
|
||||
|
||||
@libre @positive
|
||||
@@ -107,7 +145,7 @@ Feature: API Validation
|
||||
And the response file should have extension ".pdf"
|
||||
And the response file should have size greater than 100
|
||||
|
||||
@compress @qpdf @positive
|
||||
@compress @ghostscript @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
@@ -118,7 +156,7 @@ Feature: API Validation
|
||||
And the response file should have extension ".pdf"
|
||||
And the response file should have size greater than 100
|
||||
|
||||
@compress @qpdf @positive
|
||||
@compress @ghostscript @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
@@ -131,7 +169,7 @@ Feature: API Validation
|
||||
And the response file should have size greater than 100
|
||||
|
||||
|
||||
@compress @qpdf @positive
|
||||
@compress @ghostscript @positive
|
||||
Scenario: Compress
|
||||
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
|
||||
And the request data includes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security-Fat
|
||||
image: stirlingtools/stirling-pdf:latest-fat
|
||||
image: frooodle/s-pdf:latest-fat
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Security
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite-Security
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF-Ultra-Lite
|
||||
image: stirlingtools/stirling-pdf:latest-ultra-lite
|
||||
image: frooodle/s-pdf:latest-ultra-lite
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
stirling-pdf:
|
||||
container_name: Stirling-PDF
|
||||
image: stirlingtools/stirling-pdf:latest
|
||||
image: frooodle/s-pdf:latest
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/bin/bash
|
||||
# scripts/create-mac-launcher.sh
|
||||
|
||||
# Create app structure
|
||||
APP_NAME="Stirling-PDF"
|
||||
APP_BUNDLE="$APP_NAME.app"
|
||||
mkdir -p "$APP_BUNDLE/Contents/"{MacOS,Resources,Java}
|
||||
|
||||
# Convert icon
|
||||
sips -s format icns "src/main/resources/static/favicon.ico" --out "$APP_BUNDLE/Contents/Resources/AppIcon.icns"
|
||||
|
||||
# Create Info.plist
|
||||
cat > "$APP_BUNDLE/Contents/Info.plist" << EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.frooodle.stirlingpdf</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$APP_NAME</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.10.0</string>
|
||||
<key>LSEnvironment</key>
|
||||
<dict>
|
||||
<key>BROWSER_OPEN</key>
|
||||
<string>true</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Create launcher script
|
||||
cat > "$APP_BUNDLE/Contents/MacOS/$APP_NAME" << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
MIN_JAVA_VERSION="17"
|
||||
PREFERRED_JAVA_VERSION="21"
|
||||
JAVA_DOWNLOAD_URL="https://download.oracle.com/java/21/latest/jdk-21_macos-x64_bin.dmg"
|
||||
|
||||
# Check Java version
|
||||
if type -p java > /dev/null; then
|
||||
_java=java
|
||||
elif [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
_java="$JAVA_HOME/bin/java"
|
||||
else
|
||||
osascript -e 'display dialog "Java not found. Please install Java 21." buttons {"Download Java", "Cancel"} default button "Download Java"' \
|
||||
&& open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$("$_java" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
|
||||
if [[ "$version" -lt "$MIN_JAVA_VERSION" ]]; then
|
||||
osascript -e 'display dialog "Wrong Java version. Please install Java 21." buttons {"Download Java", "Cancel"} default button "Download Java"' \
|
||||
&& open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run application
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
exec java -jar "$DIR/../Java/Stirling-PDF.jar" "$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
|
||||
|
||||
# Copy JAR file
|
||||
cp "build/libs/Stirling-PDF.jar" "$APP_BUNDLE/Contents/Java/"
|
||||
@@ -1,36 +0,0 @@
|
||||
#!/bin/bash
|
||||
# scripts/create-unix-launcher.sh
|
||||
|
||||
cat > launcher.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
APP_NAME="Stirling-PDF"
|
||||
MIN_JAVA_VERSION="17"
|
||||
PREFERRED_JAVA_VERSION="21"
|
||||
JAVA_DOWNLOAD_URL="https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.tar.gz"
|
||||
BROWSER_OPEN="true"
|
||||
|
||||
# Check Java version
|
||||
if type -p java > /dev/null; then
|
||||
_java=java
|
||||
elif [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
_java="$JAVA_HOME/bin/java"
|
||||
else
|
||||
echo "Java not found. Please install Java 21."
|
||||
xdg-open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$("$_java" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
|
||||
if [[ "$version" -lt "$MIN_JAVA_VERSION" ]]; then
|
||||
echo "Java version $version detected. Please install Java 21."
|
||||
xdg-open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run application
|
||||
exec java -jar "$(dirname "$0")/Stirling-PDF.jar" "$@"
|
||||
EOF
|
||||
|
||||
chmod +x launcher.sh
|
||||
@@ -3,11 +3,6 @@ ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[az_AZ]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
]
|
||||
|
||||
[bg_BG]
|
||||
ignore = [
|
||||
'language.direction',
|
||||
@@ -37,25 +32,21 @@ ignore = [
|
||||
ignore = [
|
||||
'AddStampRequest.alphabet',
|
||||
'AddStampRequest.position',
|
||||
'home.pipeline.title'
|
||||
'PDFToBook.selectText.1',
|
||||
'PDFToText.tags',
|
||||
'addPageNumbers.selectText.3',
|
||||
'alphabet',
|
||||
'certSign.name',
|
||||
'fileChooser.dragAndDrop',
|
||||
'home.pipeline.title',
|
||||
'language.direction',
|
||||
'legal.impressum',
|
||||
'licenses.version',
|
||||
'pipeline.title',
|
||||
'pipelineOptions.pipelineHeader',
|
||||
'pro',
|
||||
'sponsor',
|
||||
'text',
|
||||
'validateSignature.cert.bits',
|
||||
'validateSignature.cert.version',
|
||||
'validateSignature.status',
|
||||
'watermark.type.1',
|
||||
'certSign.name',
|
||||
]
|
||||
|
||||
[el_GR]
|
||||
@@ -66,6 +57,7 @@ ignore = [
|
||||
[es_ES]
|
||||
ignore = [
|
||||
'adminUserSettings.roles',
|
||||
'color',
|
||||
'error',
|
||||
'language.direction',
|
||||
'no',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
translation_key="pdfToPDFA.credit"
|
||||
old_value="qpdf"
|
||||
new_value="liibreoffice"
|
||||
old_value="OCRmyPDF"
|
||||
new_value="ghostscript"
|
||||
|
||||
for file in ../src/main/resources/messages_*.properties; do
|
||||
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Configuration
|
||||
MIN_JAVA_VERSION="17"
|
||||
PREFERRED_JAVA_VERSION="21"
|
||||
JAVA_DOWNLOAD_URL="https://download.oracle.com/java/21/latest/jdk-21_linux-x64_bin.tar.gz"
|
||||
BROWSER_OPEN="true"
|
||||
|
||||
# Check Java version
|
||||
if type -p java > /dev/null; then
|
||||
_java=java
|
||||
elif [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
|
||||
_java="$JAVA_HOME/bin/java"
|
||||
else
|
||||
echo "Java not found. Please install Java 21."
|
||||
xdg-open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$("$_java" -version 2>&1 | awk -F '"' '/version/ {print $2}' | cut -d'.' -f1)
|
||||
if [[ "$version" -lt "$MIN_JAVA_VERSION" ]]; then
|
||||
echo "Java version $version detected. Please install Java 21."
|
||||
xdg-open "$JAVA_DOWNLOAD_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run application
|
||||
exec java -jar "$(dirname "$0")/Stirling-PDF.jar" "$@"
|
||||
@@ -3,14 +3,13 @@ package stirling.software.SPDF.EE;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
@Lazy
|
||||
@Slf4j
|
||||
public class EEAppConfig {
|
||||
|
||||
|
||||
@@ -25,10 +25,9 @@ public class LicenseKeyChecker {
|
||||
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
|
||||
this.licenseService = licenseService;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.checkLicense();
|
||||
}
|
||||
|
||||
@Scheduled(initialDelay = 604800000,fixedRate = 604800000) // 7 days in milliseconds
|
||||
@Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
|
||||
public void checkLicensePeriodically() {
|
||||
checkLicense();
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("OpenCV", "extract-image-scans");
|
||||
|
||||
// LibreOffice
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
addEndpointToGroup("LibreOffice", "repair");
|
||||
addEndpointToGroup("LibreOffice", "file-to-pdf");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-word");
|
||||
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
|
||||
@@ -199,11 +199,10 @@ public class EndpointConfiguration {
|
||||
// Unoconv
|
||||
addEndpointToGroup("Unoconv", "file-to-pdf");
|
||||
|
||||
// qpdf
|
||||
addEndpointToGroup("qpdf", "compress-pdf");
|
||||
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||
|
||||
addEndpointToGroup("tesseract", "ocr-pdf");
|
||||
// OCRmyPDF
|
||||
addEndpointToGroup("OCRmyPDF", "compress-pdf");
|
||||
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
|
||||
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
|
||||
|
||||
// Java
|
||||
addEndpointToGroup("Java", "merge-pdfs");
|
||||
@@ -249,10 +248,10 @@ public class EndpointConfiguration {
|
||||
addEndpointToGroup("Javascript", "compare");
|
||||
addEndpointToGroup("Javascript", "adjust-contrast");
|
||||
|
||||
// qpdf dependent endpoints
|
||||
addEndpointToGroup("qpdf", "compress-pdf");
|
||||
addEndpointToGroup("qpdf", "pdf-to-pdfa");
|
||||
addEndpointToGroup("qpdf", "repair");
|
||||
// Ghostscript dependent endpoints
|
||||
addEndpointToGroup("Ghostscript", "compress-pdf");
|
||||
addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
|
||||
addEndpointToGroup("Ghostscript", "repair");
|
||||
|
||||
// Weasyprint dependent endpoints
|
||||
addEndpointToGroup("Weasyprint", "html-to-pdf");
|
||||
|
||||
@@ -37,12 +37,12 @@ public class ExternalAppDepConfig {
|
||||
private final Map<String, List<String>> commandToGroupMapping =
|
||||
new HashMap<>() {
|
||||
{
|
||||
put("gs", List.of("Ghostscript"));
|
||||
put("soffice", List.of("LibreOffice"));
|
||||
put("ocrmypdf", List.of("OCRmyPDF"));
|
||||
put("weasyprint", List.of("Weasyprint"));
|
||||
put("pdftohtml", List.of("Pdftohtml"));
|
||||
put("unoconv", List.of("Unoconv"));
|
||||
put("qpdf", List.of("qpdf"));
|
||||
put("tesseract", List.of("tesseract"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -97,9 +97,9 @@ public class ExternalAppDepConfig {
|
||||
public void checkDependencies() {
|
||||
|
||||
// Check core dependencies
|
||||
checkDependencyAndDisableGroup("tesseract");
|
||||
checkDependencyAndDisableGroup("gs");
|
||||
checkDependencyAndDisableGroup("soffice");
|
||||
checkDependencyAndDisableGroup("qpdf");
|
||||
checkDependencyAndDisableGroup("ocrmypdf");
|
||||
checkDependencyAndDisableGroup("weasyprint");
|
||||
checkDependencyAndDisableGroup("pdftohtml");
|
||||
checkDependencyAndDisableGroup("unoconv");
|
||||
|
||||
@@ -30,7 +30,6 @@ public class InitialSecuritySetup {
|
||||
initializeAdminUser();
|
||||
} else {
|
||||
databaseBackupHelper.exportDatabase();
|
||||
userService.migrateOauth2ToSSO();
|
||||
}
|
||||
initializeInternalApiUser();
|
||||
}
|
||||
|
||||
@@ -3,16 +3,14 @@ package stirling.software.SPDF.config.security;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import org.opensaml.saml.saml2.core.AuthnRequest;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
@@ -34,8 +32,7 @@ import org.springframework.security.saml2.provider.service.authentication.OpenSa
|
||||
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
|
||||
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
|
||||
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
|
||||
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
|
||||
@@ -44,7 +41,6 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.savedrequest.NullRequestCache;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
|
||||
@@ -68,7 +64,6 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@Slf4j
|
||||
@DependsOn("runningEE")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Autowired private CustomUserDetailsService userDetailsService;
|
||||
@@ -84,10 +79,6 @@ public class SecurityConfiguration {
|
||||
@Qualifier("loginEnabled")
|
||||
public boolean loginEnabledValue;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("runningEE")
|
||||
public boolean runningEE;
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
|
||||
@@ -99,14 +90,13 @@ public class SecurityConfiguration {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
}
|
||||
|
||||
if (loginEnabledValue) {
|
||||
http.addFilterBefore(
|
||||
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
} else {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
@@ -255,22 +245,12 @@ public class SecurityConfiguration {
|
||||
}
|
||||
|
||||
// Handle SAML
|
||||
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
|
||||
// Configure the authentication provider
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
|
||||
http.authenticationProvider(authenticationProvider)
|
||||
.saml2Login(
|
||||
saml2 -> {
|
||||
try {
|
||||
if (applicationProperties.getSecurity().isSaml2Activ()
|
||||
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
|
||||
http.authenticationProvider(samlAuthenticationProvider());
|
||||
http.saml2Login(
|
||||
saml2 ->
|
||||
saml2.loginPage("/saml2")
|
||||
.relyingPartyRegistrationRepository(
|
||||
relyingPartyRegistrations())
|
||||
.authenticationManager(
|
||||
new ProviderManager(authenticationProvider))
|
||||
.successHandler(
|
||||
new CustomSaml2AuthenticationSuccessHandler(
|
||||
loginAttemptService,
|
||||
@@ -278,18 +258,14 @@ public class SecurityConfiguration {
|
||||
userService))
|
||||
.failureHandler(
|
||||
new CustomSaml2AuthenticationFailureHandler())
|
||||
.authenticationRequestResolver(
|
||||
authenticationRequestResolver(
|
||||
relyingPartyRegistrations()));
|
||||
} catch (Exception e) {
|
||||
log.error("Error configuring SAML2 login", e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
.permitAll())
|
||||
.addFilterBefore(
|
||||
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
|
||||
}
|
||||
|
||||
} else {
|
||||
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
if (applicationProperties.getSecurity().getCsrfDisabled()) {
|
||||
http.csrf(csrf -> csrf.disable());
|
||||
} else {
|
||||
CookieCsrfTokenRepository cookieRepo =
|
||||
CookieCsrfTokenRepository.withHttpOnlyFalse();
|
||||
CsrfTokenRequestAttributeHandler requestHandler =
|
||||
@@ -306,6 +282,20 @@ public class SecurityConfiguration {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public AuthenticationProvider samlAuthenticationProvider() {
|
||||
OpenSaml4AuthenticationProvider authenticationProvider =
|
||||
new OpenSaml4AuthenticationProvider();
|
||||
authenticationProvider.setResponseAuthenticationConverter(
|
||||
new CustomSaml2ResponseAuthenticationConverter(userService));
|
||||
return authenticationProvider;
|
||||
}
|
||||
|
||||
// Client Registration Repository for OAUTH2 OIDC Login
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
value = "security.oauth2.enabled",
|
||||
@@ -442,12 +432,11 @@ public class SecurityConfiguration {
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
|
||||
|
||||
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
|
||||
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
|
||||
Resource privateKeyResource = samlConf.getPrivateKey();
|
||||
|
||||
Resource certificateResource = samlConf.getSpCert();
|
||||
|
||||
Saml2X509Credential signingCredential =
|
||||
@@ -456,97 +445,26 @@ public class SecurityConfiguration {
|
||||
CertificateUtils.readCertificate(certificateResource),
|
||||
Saml2X509CredentialType.SIGNING);
|
||||
|
||||
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
|
||||
|
||||
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
|
||||
|
||||
RelyingPartyRegistration rp =
|
||||
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
|
||||
.signingX509Credentials(c -> c.add(signingCredential))
|
||||
.assertingPartyMetadata(
|
||||
metadata ->
|
||||
metadata.entityId(samlConf.getIdpIssuer())
|
||||
.signingX509Credentials((c) -> c.add(signingCredential))
|
||||
.assertingPartyDetails(
|
||||
(details) ->
|
||||
details.entityId(samlConf.getIdpIssuer())
|
||||
.singleSignOnServiceLocation(
|
||||
samlConf.getIdpSingleLoginUrl())
|
||||
.verificationX509Credentials(
|
||||
c -> c.add(verificationCredential))
|
||||
.singleSignOnServiceBinding(
|
||||
Saml2MessageBinding.POST)
|
||||
(c) -> c.add(verificationCredential))
|
||||
.wantAuthnRequestsSigned(true))
|
||||
.build();
|
||||
|
||||
return new InMemoryRelyingPartyRegistrationRepository(rp);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(
|
||||
name = "security.saml2.enabled",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
|
||||
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
|
||||
OpenSaml4AuthenticationRequestResolver resolver =
|
||||
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
|
||||
resolver.setAuthnRequestCustomizer(
|
||||
customizer -> {
|
||||
log.debug("Customizing SAML Authentication request");
|
||||
|
||||
AuthnRequest authnRequest = customizer.getAuthnRequest();
|
||||
log.debug("AuthnRequest ID: {}", authnRequest.getID());
|
||||
|
||||
if (authnRequest.getID() == null) {
|
||||
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
|
||||
}
|
||||
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
|
||||
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
|
||||
log.debug(
|
||||
"AuthnRequest Issuer: {}",
|
||||
authnRequest.getIssuer() != null
|
||||
? authnRequest.getIssuer().getValue()
|
||||
: "null");
|
||||
|
||||
HttpServletRequest request = customizer.getRequest();
|
||||
|
||||
// Log HTTP request details
|
||||
log.debug("HTTP Request Method: {}", request.getMethod());
|
||||
log.debug("Request URI: {}", request.getRequestURI());
|
||||
log.debug("Request URL: {}", request.getRequestURL().toString());
|
||||
log.debug("Query String: {}", request.getQueryString());
|
||||
log.debug("Remote Address: {}", request.getRemoteAddr());
|
||||
|
||||
// Log headers
|
||||
Collections.list(request.getHeaderNames())
|
||||
.forEach(
|
||||
headerName -> {
|
||||
log.debug(
|
||||
"Header - {}: {}",
|
||||
headerName,
|
||||
request.getHeader(headerName));
|
||||
});
|
||||
|
||||
// Log SAML specific parameters
|
||||
log.debug("SAML Request Parameters:");
|
||||
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
|
||||
log.debug("RelayState: {}", request.getParameter("RelayState"));
|
||||
|
||||
// Log session debugrmation if exists
|
||||
if (request.getSession(false) != null) {
|
||||
log.debug("Session ID: {}", request.getSession().getId());
|
||||
}
|
||||
|
||||
// Log any assertions consumer service details if present
|
||||
if (authnRequest.getAssertionConsumerServiceURL() != null) {
|
||||
log.debug(
|
||||
"AssertionConsumerServiceURL: {}",
|
||||
authnRequest.getAssertionConsumerServiceURL());
|
||||
}
|
||||
|
||||
// Log NameID policy if present
|
||||
if (authnRequest.getNameIDPolicy() != null) {
|
||||
log.debug(
|
||||
"NameIDPolicy Format: {}",
|
||||
authnRequest.getNameIDPolicy().getFormat());
|
||||
}
|
||||
});
|
||||
return resolver;
|
||||
}
|
||||
|
||||
public DaoAuthenticationProvider daoAuthenticationProvider() {
|
||||
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
|
||||
provider.setUserDetailsService(userDetailsService);
|
||||
|
||||
@@ -18,7 +18,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||
@@ -51,19 +50,8 @@ public class UserService implements UserServiceInterface {
|
||||
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
@Transactional
|
||||
public void migrateOauth2ToSSO() {
|
||||
userRepository
|
||||
.findByAuthenticationTypeIgnoreCase("OAUTH2")
|
||||
.forEach(
|
||||
user -> {
|
||||
user.setAuthenticationType(AuthenticationType.SSO);
|
||||
userRepository.save(user);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle OAUTH2 login and user auto creation.
|
||||
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
|
||||
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
|
||||
throws IllegalArgumentException, IOException {
|
||||
if (!isUsernameValid(username)) {
|
||||
return false;
|
||||
@@ -73,7 +61,7 @@ public class UserService implements UserServiceInterface {
|
||||
return true;
|
||||
}
|
||||
if (autoCreateUser) {
|
||||
saveUser(username, AuthenticationType.SSO);
|
||||
saveUser(username, AuthenticationType.OAUTH2);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -34,12 +34,6 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
@Value("${spring.datasource.url}")
|
||||
private String url;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String databaseUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String databasePassword;
|
||||
|
||||
private Path backupPath = Paths.get("configs/db/backup/");
|
||||
|
||||
@Override
|
||||
@@ -140,8 +134,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
@@ -154,8 +147,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
// Retrieves the H2 database version.
|
||||
public String getH2Version() {
|
||||
String version = "Unknown";
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||
if (rs.next()) {
|
||||
@@ -197,8 +189,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
private boolean executeDatabaseScript(Path scriptPath) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
try (Connection conn = DriverManager.getConnection(url, "sa", "");
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, scriptPath.toString());
|
||||
stmt.execute();
|
||||
|
||||
@@ -82,7 +82,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
}
|
||||
if (userService.usernameExistsIgnoreCase(username)
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO)
|
||||
&& !userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.OAUTH2)
|
||||
&& oAuth.getAutoCreateUser()) {
|
||||
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
@@ -94,7 +95,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
return;
|
||||
}
|
||||
if (principal instanceof OAuth2User) {
|
||||
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser());
|
||||
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
|
||||
}
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
package stirling.software.SPDF.config.security.saml2;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
|
||||
import org.bouncycastle.util.io.pem.PemObject;
|
||||
import org.bouncycastle.util.io.pem.PemReader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CertificateUtils {
|
||||
|
||||
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
|
||||
@@ -30,26 +35,95 @@ public class CertificateUtils {
|
||||
}
|
||||
|
||||
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
|
||||
try (PEMParser pemParser =
|
||||
new PEMParser(
|
||||
try (PemReader pemReader =
|
||||
new PemReader(
|
||||
new InputStreamReader(
|
||||
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
|
||||
Object object = pemParser.readObject();
|
||||
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
|
||||
|
||||
if (object instanceof PEMKeyPair) {
|
||||
// Handle traditional RSA private key format
|
||||
PEMKeyPair keypair = (PEMKeyPair) object;
|
||||
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
|
||||
} else if (object instanceof PrivateKeyInfo) {
|
||||
// Handle PKCS#8 format
|
||||
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported key format: "
|
||||
+ (object != null ? object.getClass().getName() : "null"));
|
||||
}
|
||||
PemObject pemObject = pemReader.readPemObject();
|
||||
byte[] decodedKey = pemObject.getContent();
|
||||
return (RSAPrivateKey)
|
||||
KeyFactory.getInstance("RSA")
|
||||
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
|
||||
|
||||
if (certificateResource instanceof UrlResource) {
|
||||
return extractCertificateFromMetadata(certificateResource);
|
||||
} else {
|
||||
// Treat as file resource
|
||||
return readCertificate(certificateResource);
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
|
||||
log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
|
||||
|
||||
try (InputStream is = metadataResource.getInputStream()) {
|
||||
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
|
||||
log.info("Retrieved metadata content, length: {}", content.length());
|
||||
|
||||
// Find the certificate data
|
||||
int startIndex = content.indexOf("<ds:X509Certificate>");
|
||||
int endIndex = content.indexOf("</ds:X509Certificate>");
|
||||
|
||||
if (startIndex == -1 || endIndex == -1) {
|
||||
log.error("Certificate tags not found in metadata");
|
||||
throw new Exception("Certificate tags not found in metadata");
|
||||
}
|
||||
|
||||
// Extract certificate data
|
||||
String certData = content.substring(
|
||||
startIndex + "<ds:X509Certificate>".length(),
|
||||
endIndex
|
||||
).trim();
|
||||
|
||||
log.info("Found certificate data, length: {}", certData.length());
|
||||
|
||||
// Remove any whitespace and newlines from cert data
|
||||
certData = certData.replaceAll("\\s+", "");
|
||||
|
||||
// Reconstruct PEM format with proper line breaks
|
||||
StringBuilder pemBuilder = new StringBuilder();
|
||||
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
|
||||
|
||||
// Insert line breaks every 64 characters
|
||||
int lineLength = 64;
|
||||
for (int i = 0; i < certData.length(); i += lineLength) {
|
||||
int end = Math.min(i + lineLength, certData.length());
|
||||
pemBuilder.append(certData, i, end).append('\n');
|
||||
}
|
||||
|
||||
pemBuilder.append("-----END CERTIFICATE-----");
|
||||
String pemCert = pemBuilder.toString();
|
||||
|
||||
log.debug("Reconstructed PEM certificate:\n{}", pemCert);
|
||||
|
||||
try {
|
||||
ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
|
||||
|
||||
log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
|
||||
|
||||
// Optional: check validity dates
|
||||
cert.checkValidity(); // Throws CertificateExpiredException if expired
|
||||
log.info("Certificate is valid (not expired)");
|
||||
|
||||
return cert;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse certificate", e);
|
||||
throw new Exception("Failed to parse X509 certificate from metadata", e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing metadata resource", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -16,23 +16,35 @@ import lombok.extern.slf4j.Slf4j;
|
||||
@Slf4j
|
||||
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
if (exception instanceof Saml2AuthenticationException) {
|
||||
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||
} else if (exception instanceof ProviderNotFoundException) {
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?erroroauth=not_authentication_provider_found");
|
||||
}
|
||||
log.error("AuthenticationException: " + exception);
|
||||
}
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception)
|
||||
throws IOException, ServletException {
|
||||
|
||||
if (exception instanceof Saml2AuthenticationException saml2Exception) {
|
||||
Saml2Error error = saml2Exception.getSaml2Error();
|
||||
|
||||
// Log detailed information about the SAML error
|
||||
log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
|
||||
log.error("Error description: {}", error.getDescription());
|
||||
|
||||
// Redirect to login with specific error code
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
|
||||
} else if (exception instanceof ProviderNotFoundException) {
|
||||
log.error("Authentication failed: No authentication provider found");
|
||||
|
||||
getRedirectStrategy()
|
||||
.sendRedirect(
|
||||
request,
|
||||
response,
|
||||
"/login?erroroauth=not_authentication_provider_found");
|
||||
} else {
|
||||
log.error("Unknown AuthenticationException: {}", exception.getMessage());
|
||||
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
@@ -21,11 +20,11 @@ import stirling.software.SPDF.model.AuthenticationType;
|
||||
import stirling.software.SPDF.utils.RequestUriUtils;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Slf4j
|
||||
public class CustomSaml2AuthenticationSuccessHandler
|
||||
extends SavedRequestAwareAuthenticationSuccessHandler {
|
||||
|
||||
private LoginAttemptService loginAttemptService;
|
||||
|
||||
private ApplicationProperties applicationProperties;
|
||||
private UserService userService;
|
||||
|
||||
@@ -35,12 +34,10 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
throws ServletException, IOException {
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
log.debug("Starting SAML2 authentication success handling");
|
||||
|
||||
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
|
||||
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
|
||||
log.debug("Authenticated principal found for user: {}", username);
|
||||
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
String contextPath = request.getContextPath();
|
||||
SavedRequest savedRequest =
|
||||
@@ -48,77 +45,46 @@ public class CustomSaml2AuthenticationSuccessHandler
|
||||
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
|
||||
: null;
|
||||
|
||||
log.debug(
|
||||
"Session exists: {}, Saved request exists: {}",
|
||||
session != null,
|
||||
savedRequest != null);
|
||||
|
||||
if (savedRequest != null
|
||||
&& !RequestUriUtils.isStaticResource(
|
||||
contextPath, savedRequest.getRedirectUrl())) {
|
||||
log.debug(
|
||||
"Valid saved request found, redirecting to original destination: {}",
|
||||
savedRequest.getRedirectUrl());
|
||||
// Redirect to the original destination
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
} else {
|
||||
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
|
||||
log.debug(
|
||||
"Processing SAML2 authentication with autoCreateUser: {}",
|
||||
saml2.getAutoCreateUser());
|
||||
|
||||
if (loginAttemptService.isBlocked(username)) {
|
||||
log.debug("User {} is blocked due to too many login attempts", username);
|
||||
if (session != null) {
|
||||
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
|
||||
}
|
||||
throw new LockedException(
|
||||
"Your account has been locked due to too many failed login attempts.");
|
||||
}
|
||||
|
||||
boolean userExists = userService.usernameExistsIgnoreCase(username);
|
||||
boolean hasPassword = userExists && userService.hasPassword(username);
|
||||
boolean isSSOUser =
|
||||
userExists
|
||||
&& userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.SSO);
|
||||
|
||||
log.debug(
|
||||
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
|
||||
userExists,
|
||||
hasPassword,
|
||||
isSSOUser);
|
||||
|
||||
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
|
||||
log.debug(
|
||||
"User {} exists with password but is not SSO user, redirecting to logout",
|
||||
username);
|
||||
if (userService.usernameExistsIgnoreCase(username)
|
||||
&& userService.hasPassword(username)
|
||||
&& !userService.isAuthenticationTypeByUsername(
|
||||
username, AuthenticationType.OAUTH2)
|
||||
&& saml2.getAutoCreateUser()) {
|
||||
response.sendRedirect(
|
||||
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (saml2.getBlockRegistration() && !userExists) {
|
||||
log.debug("Registration blocked for new user: {}", username);
|
||||
if (saml2.getBlockRegistration()
|
||||
&& !userService.usernameExistsIgnoreCase(username)) {
|
||||
response.sendRedirect(
|
||||
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
|
||||
return;
|
||||
}
|
||||
log.debug("Processing SSO post-login for user: {}", username);
|
||||
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
|
||||
log.debug("Successfully processed authentication for user: {}", username);
|
||||
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
|
||||
response.sendRedirect(contextPath + "/");
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.debug(
|
||||
"Invalid username detected for user: {}, redirecting to logout",
|
||||
username);
|
||||
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("Non-SAML2 principal detected, delegating to parent handler");
|
||||
super.onAuthenticationSuccess(request, response, authentication);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package stirling.software.SPDF.config.security.saml2;
|
||||
import java.util.*;
|
||||
|
||||
import org.opensaml.core.xml.XMLObject;
|
||||
import org.opensaml.core.xml.schema.XSBoolean;
|
||||
import org.opensaml.core.xml.schema.XSString;
|
||||
import org.opensaml.saml.saml2.core.Assertion;
|
||||
import org.opensaml.saml.saml2.core.Attribute;
|
||||
import org.opensaml.saml.saml2.core.AttributeStatement;
|
||||
@@ -28,60 +30,15 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
// Get the text content directly
|
||||
String value = xmlObject.getDOM().getTextContent();
|
||||
if (value != null && !value.trim().isEmpty()) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!values.isEmpty()) {
|
||||
// Store with both full URI and last part of the URI
|
||||
attributes.put(attributeName, values);
|
||||
String shortName = attributeName.substring(attributeName.lastIndexOf('/') + 1);
|
||||
attributes.put(shortName, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Saml2Authentication convert(ResponseToken responseToken) {
|
||||
// Extract the assertion from the response
|
||||
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Debug log with actual values
|
||||
log.debug("Extracted SAML Attributes: " + attributes);
|
||||
// Extract the NameID
|
||||
String nameId = assertion.getSubject().getNameID().getValue();
|
||||
|
||||
// Try to get username/identifier in order of preference
|
||||
String userIdentifier = null;
|
||||
if (hasAttribute(attributes, "username")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "username");
|
||||
} else if (hasAttribute(attributes, "emailaddress")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "emailaddress");
|
||||
} else if (hasAttribute(attributes, "name")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "name");
|
||||
} else if (hasAttribute(attributes, "upn")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "upn");
|
||||
} else if (hasAttribute(attributes, "uid")) {
|
||||
userIdentifier = getFirstAttributeValue(attributes, "uid");
|
||||
} else {
|
||||
userIdentifier = assertion.getSubject().getNameID().getValue();
|
||||
}
|
||||
|
||||
// Rest of your existing code...
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(userIdentifier);
|
||||
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
|
||||
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
|
||||
if (userOpt.isPresent()) {
|
||||
User user = userOpt.get();
|
||||
@@ -91,27 +48,39 @@ public class CustomSaml2ResponseAuthenticationConverter
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the SessionIndexes
|
||||
List<String> sessionIndexes = new ArrayList<>();
|
||||
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
|
||||
sessionIndexes.add(authnStatement.getSessionIndex());
|
||||
}
|
||||
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
new CustomSaml2AuthenticatedPrincipal(
|
||||
userIdentifier, attributes, userIdentifier, sessionIndexes);
|
||||
// Extract the Attributes
|
||||
Map<String, List<Object>> attributes = extractAttributes(assertion);
|
||||
|
||||
// Create the custom principal
|
||||
CustomSaml2AuthenticatedPrincipal principal =
|
||||
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
|
||||
|
||||
// Create the Saml2Authentication
|
||||
return new Saml2Authentication(
|
||||
principal,
|
||||
responseToken.getToken().getSaml2Response(),
|
||||
Collections.singletonList(simpleGrantedAuthority));
|
||||
}
|
||||
|
||||
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
|
||||
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
|
||||
}
|
||||
|
||||
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
|
||||
List<Object> values = attributes.get(name);
|
||||
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
|
||||
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
|
||||
Map<String, List<Object>> attributes = new HashMap<>();
|
||||
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
|
||||
for (Attribute attribute : attributeStatement.getAttributes()) {
|
||||
String attributeName = attribute.getName();
|
||||
List<Object> values = new ArrayList<>();
|
||||
for (XMLObject xmlObject : attribute.getAttributeValues()) {
|
||||
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
|
||||
values.add(((XSString) xmlObject).getValue());
|
||||
}
|
||||
attributes.put(attributeName, values);
|
||||
}
|
||||
}
|
||||
return attributes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package stirling.software.SPDF.config.security.saml2;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
|
||||
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.saml2.core.Saml2ErrorCodes;
|
||||
|
||||
public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
|
||||
private final OpenSaml4AuthenticationProvider delegate;
|
||||
|
||||
public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
if (authentication instanceof Saml2AuthenticationToken token) {
|
||||
String samlResponse = token.getSaml2Response();
|
||||
|
||||
// Log the raw SAML response
|
||||
log.info("Raw SAML Response (Base64): {}", samlResponse);
|
||||
|
||||
// Decode and log the SAML response XML
|
||||
try {
|
||||
String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
|
||||
log.info("Decoded SAML Response XML:\n{}", decodedResponse);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// If decoding fails, it’s likely already plain XML
|
||||
log.warn("SAML Response appears to be different format, not Base64-encoded.");
|
||||
log.debug("SAML Response XML:\n{}", samlResponse);
|
||||
}
|
||||
// Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
|
||||
try {
|
||||
return delegate.authenticate(authentication);
|
||||
} catch (Saml2AuthenticationException e) {
|
||||
log.error("SAML authentication failed: {}");
|
||||
log.error("Detailed error message: {}", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
// Only support Saml2AuthenticationToken
|
||||
return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
|
||||
}
|
||||
}
|
||||
@@ -244,8 +244,8 @@ public class UserController {
|
||||
return new RedirectView("/addUsers?messageType=invalidRole", true);
|
||||
}
|
||||
|
||||
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
|
||||
userService.saveUser(username, AuthenticationType.SSO, role);
|
||||
if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
|
||||
userService.saveUser(username, AuthenticationType.OAUTH2, role);
|
||||
} else {
|
||||
if (password.isBlank()) {
|
||||
return new RedirectView("/addUsers?messageType=invalidPassword", true);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -38,90 +37,59 @@ public class ConvertPDFToPDFA {
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A",
|
||||
description =
|
||||
"This endpoint converts a PDF file to a PDF/A file using LibreOffice. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
|
||||
throws Exception {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
String outputFormat = request.getOutputFormat();
|
||||
|
||||
// Validate input file type
|
||||
if (!"application/pdf".equals(inputFile.getContentType())) {
|
||||
logger.error("Invalid input file type: {}", inputFile.getContentType());
|
||||
throw new IllegalArgumentException("Input file must be a PDF");
|
||||
// Convert MultipartFile to byte[]
|
||||
byte[] pdfBytes = inputFile.getBytes();
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
try (OutputStream outputStream = new FileOutputStream(tempInputFile.toFile())) {
|
||||
outputStream.write(pdfBytes);
|
||||
}
|
||||
|
||||
// Get the original filename without extension
|
||||
String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename());
|
||||
if (originalFileName == null || originalFileName.trim().isEmpty()) {
|
||||
originalFileName = "output.pdf";
|
||||
}
|
||||
String baseFileName =
|
||||
originalFileName.contains(".")
|
||||
? originalFileName.substring(0, originalFileName.lastIndexOf('.'))
|
||||
: originalFileName;
|
||||
// Prepare the output file path
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
|
||||
Path tempInputFile = null;
|
||||
Path tempOutputDir = null;
|
||||
byte[] fileBytes;
|
||||
// Prepare the ghostscript command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("gs");
|
||||
command.add("-dPDFA=" + ("pdfa".equals(outputFormat) ? "2" : "1"));
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sColorConversionStrategy=sRGB");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dPDFACompatibilityPolicy=2");
|
||||
command.add("-o");
|
||||
command.add(tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (returnCode.getRc() != 0) {
|
||||
logger.info(
|
||||
outputFormat + " conversion failed with return code: " + returnCode.getRc());
|
||||
}
|
||||
|
||||
try {
|
||||
// Save uploaded file to temp location
|
||||
tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile);
|
||||
|
||||
// Create temp output directory
|
||||
tempOutputDir = Files.createTempDirectory("output_");
|
||||
|
||||
// Determine PDF/A filter based on requested format
|
||||
String pdfFilter =
|
||||
"pdfa".equals(outputFormat)
|
||||
? "writer_pdf_Export:{'SelectPdfVersion':{'Value':'2'}}:writer_pdf_Export"
|
||||
: "writer_pdf_Export:{'SelectPdfVersion':{'Value':'1'}}:writer_pdf_Export";
|
||||
|
||||
// Prepare LibreOffice command
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"soffice",
|
||||
"--headless",
|
||||
"--nologo",
|
||||
"--convert-to",
|
||||
"pdf:" + pdfFilter,
|
||||
"--outdir",
|
||||
tempOutputDir.toString(),
|
||||
tempInputFile.toString()));
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
if (returnCode.getRc() != 0) {
|
||||
logger.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
|
||||
throw new RuntimeException("PDF/A conversion failed");
|
||||
}
|
||||
|
||||
// Get the output file
|
||||
File[] outputFiles = tempOutputDir.toFile().listFiles();
|
||||
if (outputFiles == null || outputFiles.length != 1) {
|
||||
throw new RuntimeException(
|
||||
"Expected exactly one output file but found "
|
||||
+ (outputFiles == null ? "none" : outputFiles.length));
|
||||
}
|
||||
|
||||
fileBytes = FileUtils.readFileToByteArray(outputFiles[0]);
|
||||
String outputFilename = baseFileName + "_PDFA.pdf";
|
||||
|
||||
byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_PDFA.pdf";
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
fileBytes, outputFilename, MediaType.APPLICATION_PDF);
|
||||
|
||||
pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
if (tempInputFile != null) {
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
}
|
||||
if (tempOutputDir != null) {
|
||||
FileUtils.deleteDirectory(tempOutputDir.toFile());
|
||||
}
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package stirling.software.SPDF.controller.api.converters;
|
||||
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
import org.apache.commons.csv.QuoteMode;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
@@ -18,23 +18,21 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.opencsv.CSVWriter;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.controller.api.CropController;
|
||||
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
|
||||
import stirling.software.SPDF.model.api.extract.PDFFilePage;
|
||||
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
|
||||
import technology.tabula.ObjectExtractor;
|
||||
import technology.tabula.Page;
|
||||
import technology.tabula.Table;
|
||||
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
|
||||
import technology.tabula.writers.Writer;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/convert")
|
||||
@Tag(name = "Convert", description = "Convert APIs")
|
||||
public class ExtractCSVController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ExtractCSVController.class);
|
||||
private static final Logger logger = LoggerFactory.getLogger(CropController.class);
|
||||
|
||||
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
|
||||
@Operation(
|
||||
@@ -42,16 +40,57 @@ public class ExtractCSVController {
|
||||
description =
|
||||
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
|
||||
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
|
||||
StringWriter writer = new StringWriter();
|
||||
|
||||
ArrayList<String> tableData = new ArrayList<>();
|
||||
int columnsCount = 0;
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
|
||||
CSVFormat format =
|
||||
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build();
|
||||
Writer csvWriter = new FlexibleCSVWriter(format);
|
||||
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
|
||||
try (ObjectExtractor extractor = new ObjectExtractor(document)) {
|
||||
Page page = extractor.extract(form.getPageId());
|
||||
List<Table> tables = sea.extract(page);
|
||||
csvWriter.write(writer, tables);
|
||||
final double res = 72; // PDF units are at 72 DPI
|
||||
PDFTableStripper stripper = new PDFTableStripper();
|
||||
PDPage pdPage = document.getPage(form.getPageId() - 1);
|
||||
stripper.extractTable(pdPage);
|
||||
columnsCount = stripper.getColumns();
|
||||
for (int c = 0; c < columnsCount; ++c) {
|
||||
for (int r = 0; r < stripper.getRows(); ++r) {
|
||||
tableData.add(stripper.getText(r, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<String> notEmptyColumns = new ArrayList<>();
|
||||
|
||||
for (String item : tableData) {
|
||||
if (!item.trim().isEmpty()) {
|
||||
notEmptyColumns.add(item);
|
||||
} else {
|
||||
columnsCount--;
|
||||
}
|
||||
}
|
||||
|
||||
List<String> fullTable =
|
||||
notEmptyColumns.stream()
|
||||
.map(
|
||||
(entity) ->
|
||||
entity.replace('\n', ' ')
|
||||
.replace('\r', ' ')
|
||||
.trim()
|
||||
.replaceAll("\\s{2,}", "|"))
|
||||
.toList();
|
||||
|
||||
int rowsCount = fullTable.get(0).split("\\|").length;
|
||||
|
||||
ArrayList<String> headersList = getTableHeaders(columnsCount, fullTable);
|
||||
ArrayList<String> recordList = getRecordsList(rowsCount, fullTable);
|
||||
|
||||
if (headersList.size() == 0 && recordList.size() == 0) {
|
||||
throw new Exception("No table detected, no headers or records found");
|
||||
}
|
||||
|
||||
StringWriter writer = new StringWriter();
|
||||
try (CSVWriter csvWriter = new CSVWriter(writer)) {
|
||||
csvWriter.writeNext(headersList.toArray(new String[0]));
|
||||
for (String record : recordList) {
|
||||
csvWriter.writeNext(record.split("\\|"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,4 +107,33 @@ public class ExtractCSVController {
|
||||
|
||||
return ResponseEntity.ok().headers(headers).body(writer.toString());
|
||||
}
|
||||
|
||||
private ArrayList<String> getRecordsList(int rowsCounts, List<String> items) {
|
||||
ArrayList<String> recordsList = new ArrayList<>();
|
||||
|
||||
for (int b = 1; b < rowsCounts; b++) {
|
||||
StringBuilder strbldr = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < items.size(); i++) {
|
||||
String[] parts = items.get(i).split("\\|");
|
||||
strbldr.append(parts[b]);
|
||||
if (i != items.size() - 1) {
|
||||
strbldr.append("|");
|
||||
}
|
||||
}
|
||||
recordsList.add(strbldr.toString());
|
||||
}
|
||||
|
||||
return recordsList;
|
||||
}
|
||||
|
||||
private ArrayList<String> getTableHeaders(int columnsCount, List<String> items) {
|
||||
ArrayList<String> resultList = new ArrayList<>();
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
String[] parts = items.get(i).split("\\|");
|
||||
resultList.add(parts[0]);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.cos.COSName;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -52,54 +53,6 @@ public class CompressController {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor) throws Exception {
|
||||
byte[] fileBytes = Files.readAllBytes(pdfFile);
|
||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||
double scaleFactor = initialScaleFactor;
|
||||
|
||||
for (PDPage page : doc.getPages()) {
|
||||
PDResources res = page.getResources();
|
||||
if (res != null && res.getXObjectNames() != null) {
|
||||
for (COSName name : res.getXObjectNames()) {
|
||||
PDXObject xobj = res.getXObject(name);
|
||||
if (xobj instanceof PDImageXObject) {
|
||||
PDImageXObject image = (PDImageXObject) xobj;
|
||||
BufferedImage bufferedImage = image.getImage();
|
||||
|
||||
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
|
||||
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
|
||||
|
||||
if (newWidth == 0 || newHeight == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Image scaledImage =
|
||||
bufferedImage.getScaledInstance(
|
||||
newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||
|
||||
BufferedImage scaledBufferedImage =
|
||||
new BufferedImage(
|
||||
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
|
||||
|
||||
ByteArrayOutputStream compressedImageStream =
|
||||
new ByteArrayOutputStream();
|
||||
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
|
||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||
compressedImageStream.close();
|
||||
|
||||
PDImageXObject compressedImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
doc, imageBytes, image.getCOSObject().toString());
|
||||
res.put(name, compressedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
doc.save(pdfFile.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
|
||||
@Operation(
|
||||
summary = "Optimize PDF file",
|
||||
@@ -122,92 +75,209 @@ public class CompressController {
|
||||
autoMode = true;
|
||||
}
|
||||
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
|
||||
long inputFileSize = Files.size(tempInputFile);
|
||||
|
||||
// Prepare the output file path
|
||||
|
||||
Path tempOutputFile = null;
|
||||
byte[] pdfBytes;
|
||||
try {
|
||||
tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
|
||||
// Determine initial optimization level based on expected size reduction, only if in
|
||||
// autoMode
|
||||
if (autoMode) {
|
||||
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
|
||||
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
|
||||
if (sizeReductionRatio > 0.7) {
|
||||
optimizeLevel = 1;
|
||||
} else if (sizeReductionRatio > 0.5) {
|
||||
optimizeLevel = 2;
|
||||
} else if (sizeReductionRatio > 0.35) {
|
||||
optimizeLevel = 3;
|
||||
} else {
|
||||
optimizeLevel = 3;
|
||||
}
|
||||
}
|
||||
|
||||
boolean sizeMet = false;
|
||||
while (!sizeMet && optimizeLevel <= 9) {
|
||||
|
||||
// Apply additional image compression for levels 6-9
|
||||
if (optimizeLevel >= 6) {
|
||||
// Calculate scale factor based on optimization level
|
||||
double scaleFactor =
|
||||
switch (optimizeLevel) {
|
||||
case 6 -> 0.9; // 90% of original size
|
||||
case 7 -> 0.8; // 80% of original size
|
||||
case 8 -> 0.65; // 70% of original size
|
||||
case 9 -> 0.5; // 60% of original size
|
||||
default -> 1.0;
|
||||
};
|
||||
compressImagesInPDF(tempInputFile, scaleFactor);
|
||||
}
|
||||
|
||||
// Run QPDF optimization
|
||||
while (!sizeMet && optimizeLevel <= 4) {
|
||||
// Prepare the Ghostscript command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("qpdf");
|
||||
if (request.getNormalize()) {
|
||||
command.add("--normalize-content=y");
|
||||
command.add("gs");
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add("-dCompatibilityLevel=1.5");
|
||||
|
||||
switch (optimizeLevel) {
|
||||
case 1:
|
||||
command.add("-dPDFSETTINGS=/prepress");
|
||||
break;
|
||||
case 2:
|
||||
command.add("-dPDFSETTINGS=/printer");
|
||||
break;
|
||||
case 3:
|
||||
command.add("-dPDFSETTINGS=/ebook");
|
||||
break;
|
||||
case 4:
|
||||
command.add("-dPDFSETTINGS=/screen");
|
||||
break;
|
||||
default:
|
||||
command.add("-dPDFSETTINGS=/default");
|
||||
}
|
||||
if (request.getLinearize()) {
|
||||
command.add("--linearize");
|
||||
}
|
||||
command.add("--optimize-images");
|
||||
command.add("--recompress-flate");
|
||||
command.add("--compression-level=" + optimizeLevel);
|
||||
command.add("--compress-streams=y");
|
||||
command.add("--object-streams=generate");
|
||||
|
||||
command.add("-dNOPAUSE");
|
||||
command.add("-dQUIET");
|
||||
command.add("-dBATCH");
|
||||
command.add("-sOutputFile=" + tempOutputFile.toString());
|
||||
command.add(tempInputFile.toString());
|
||||
command.add(tempOutputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode = null;
|
||||
try {
|
||||
returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
} catch (Exception e) {
|
||||
if (returnCode != null && returnCode.getRc() != 3) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Check if file size is within expected size or not auto mode
|
||||
// Check if file size is within expected size or not auto mode so instantly finish
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
if (outputFileSize <= expectedOutputSize || !autoMode) {
|
||||
sizeMet = true;
|
||||
} else {
|
||||
optimizeLevel =
|
||||
incrementOptimizeLevel(
|
||||
optimizeLevel, outputFileSize, expectedOutputSize);
|
||||
if (autoMode && optimizeLevel > 9) {
|
||||
logger.info("Maximum compression level reached in auto mode");
|
||||
// Increase optimization level for next iteration
|
||||
optimizeLevel++;
|
||||
if (autoMode && optimizeLevel > 4) {
|
||||
logger.info("Skipping level 5 due to bad results in auto mode");
|
||||
sizeMet = true;
|
||||
} else {
|
||||
logger.info(
|
||||
"Increasing ghostscript optimisation level to " + optimizeLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedOutputSize != null && autoMode) {
|
||||
long outputFileSize = Files.size(tempOutputFile);
|
||||
byte[] fileBytes = Files.readAllBytes(tempOutputFile);
|
||||
if (outputFileSize > expectedOutputSize) {
|
||||
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
|
||||
long previousFileSize = 0;
|
||||
double scaleFactorConst = 0.9f;
|
||||
double scaleFactor = 0.9f;
|
||||
while (true) {
|
||||
for (PDPage page : doc.getPages()) {
|
||||
PDResources res = page.getResources();
|
||||
if (res != null && res.getXObjectNames() != null) {
|
||||
for (COSName name : res.getXObjectNames()) {
|
||||
PDXObject xobj = res.getXObject(name);
|
||||
if (xobj != null && xobj instanceof PDImageXObject) {
|
||||
PDImageXObject image = (PDImageXObject) xobj;
|
||||
|
||||
// Get the image in BufferedImage format
|
||||
BufferedImage bufferedImage = image.getImage();
|
||||
|
||||
// Calculate the new dimensions
|
||||
int newWidth =
|
||||
(int)
|
||||
(bufferedImage.getWidth()
|
||||
* scaleFactorConst);
|
||||
int newHeight =
|
||||
(int)
|
||||
(bufferedImage.getHeight()
|
||||
* scaleFactorConst);
|
||||
|
||||
// If the new dimensions are zero, skip this iteration
|
||||
if (newWidth == 0 || newHeight == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, proceed with the scaling
|
||||
Image scaledImage =
|
||||
bufferedImage.getScaledInstance(
|
||||
newWidth,
|
||||
newHeight,
|
||||
Image.SCALE_SMOOTH);
|
||||
|
||||
// Convert the scaled image back to a BufferedImage
|
||||
BufferedImage scaledBufferedImage =
|
||||
new BufferedImage(
|
||||
newWidth,
|
||||
newHeight,
|
||||
BufferedImage.TYPE_INT_RGB);
|
||||
scaledBufferedImage
|
||||
.getGraphics()
|
||||
.drawImage(scaledImage, 0, 0, null);
|
||||
|
||||
// Compress the scaled image
|
||||
ByteArrayOutputStream compressedImageStream =
|
||||
new ByteArrayOutputStream();
|
||||
ImageIO.write(
|
||||
scaledBufferedImage,
|
||||
"jpeg",
|
||||
compressedImageStream);
|
||||
byte[] imageBytes = compressedImageStream.toByteArray();
|
||||
compressedImageStream.close();
|
||||
|
||||
PDImageXObject compressedImage =
|
||||
PDImageXObject.createFromByteArray(
|
||||
doc,
|
||||
imageBytes,
|
||||
image.getCOSObject().toString());
|
||||
|
||||
// Replace the image in the resources with the
|
||||
// compressed
|
||||
// version
|
||||
res.put(name, compressedImage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save the document to tempOutputFile again
|
||||
doc.save(tempOutputFile.toString());
|
||||
|
||||
long currentSize = Files.size(tempOutputFile);
|
||||
// Check if the overall PDF size is still larger than expectedOutputSize
|
||||
if (currentSize > expectedOutputSize) {
|
||||
// Log the current file size and scaleFactor
|
||||
|
||||
logger.info(
|
||||
"Current file size: "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize));
|
||||
logger.info("Current scale factor: " + scaleFactor);
|
||||
|
||||
// The file is still too large, reduce scaleFactor and try again
|
||||
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
|
||||
// Avoid scaleFactor being too small, causing the image to shrink to
|
||||
// 0
|
||||
if (scaleFactor < 0.2f || previousFileSize == currentSize) {
|
||||
throw new RuntimeException(
|
||||
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
|
||||
+ FileUtils.byteCountToDisplaySize(currentSize)
|
||||
+ ", "
|
||||
+ currentSize
|
||||
+ " bytes");
|
||||
}
|
||||
previousFileSize = currentSize;
|
||||
} else {
|
||||
// The file is small enough, break the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = Files.readAllBytes(tempOutputFile);
|
||||
Path finalFile = tempOutputFile;
|
||||
|
||||
// Check if optimized file is larger than the original
|
||||
if (pdfBytes.length > inputFileSize) {
|
||||
// Log the occurrence
|
||||
logger.warn(
|
||||
"Optimized file is larger than the original. Returning the original file instead.");
|
||||
|
||||
// Read the original file again
|
||||
finalFile = tempInputFile;
|
||||
}
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
@@ -216,31 +286,10 @@ public class CompressController {
|
||||
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
|
||||
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
// deleted by multipart file handler deu to transferTo?
|
||||
// Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private int determineOptimizeLevel(double sizeReductionRatio) {
|
||||
if (sizeReductionRatio > 0.9) return 1;
|
||||
if (sizeReductionRatio > 0.8) return 2;
|
||||
if (sizeReductionRatio > 0.7) return 3;
|
||||
if (sizeReductionRatio > 0.6) return 4;
|
||||
if (sizeReductionRatio > 0.5) return 5;
|
||||
if (sizeReductionRatio > 0.4) return 6;
|
||||
if (sizeReductionRatio > 0.3) return 7;
|
||||
if (sizeReductionRatio > 0.2) return 8;
|
||||
return 9;
|
||||
}
|
||||
|
||||
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
|
||||
double currentRatio = currentSize / (double) targetSize;
|
||||
logger.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
|
||||
|
||||
if (currentRatio > 2.0) {
|
||||
return Math.min(9, currentLevel + 3);
|
||||
} else if (currentRatio > 1.5) {
|
||||
return Math.min(9, currentLevel + 2);
|
||||
}
|
||||
return Math.min(9, currentLevel + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ public class FakeScanControllerWIP {
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
description =
|
||||
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
|
||||
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.")
|
||||
public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.WebDataBinder;
|
||||
import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -28,7 +26,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@@ -47,11 +44,6 @@ public class MetadataController {
|
||||
return entry;
|
||||
}
|
||||
|
||||
@InitBinder
|
||||
public void initBinder(WebDataBinder binder) {
|
||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
|
||||
@Operation(
|
||||
summary = "Update metadata of a PDF file",
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
package stirling.software.SPDF.controller.api.misc;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -33,31 +23,24 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
import io.github.pixee.security.Filenames;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor;
|
||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||
import stirling.software.SPDF.utils.WebResponseUtils;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/misc")
|
||||
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||
@Slf4j
|
||||
public class OCRController {
|
||||
|
||||
@Autowired private ApplicationProperties applicationProperties;
|
||||
@Autowired ApplicationProperties applicationProperties;
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
/** Gets the list of available Tesseract languages from the tessdata directory */
|
||||
public List<String> getAvailableTesseractLanguages() {
|
||||
String tessdataDir = applicationProperties.getSystem().getTessdataDir();
|
||||
File[] files = new File(tessdataDir).listFiles();
|
||||
@@ -71,163 +54,196 @@ public class OCRController {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@Autowired
|
||||
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
}
|
||||
|
||||
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
|
||||
public ResponseEntity<byte[]> processPdfWithOCR(
|
||||
@ModelAttribute ProcessPdfWithOcrRequest request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
List<String> languages = request.getLanguages();
|
||||
List<String> selectedLanguages = request.getLanguages();
|
||||
Boolean sidecar = request.isSidecar();
|
||||
Boolean deskew = request.isDeskew();
|
||||
Boolean clean = request.isClean();
|
||||
Boolean cleanFinal = request.isCleanFinal();
|
||||
String ocrType = request.getOcrType();
|
||||
String ocrRenderType = request.getOcrRenderType();
|
||||
Boolean removeImagesAfter = request.isRemoveImagesAfter();
|
||||
// --output-type pdfa
|
||||
if (selectedLanguages == null || selectedLanguages.isEmpty()) {
|
||||
throw new IOException("Please select at least one language.");
|
||||
}
|
||||
|
||||
Path tempDir = Files.createTempDirectory("ocr_process");
|
||||
Path tempInputFile = tempDir.resolve("input.pdf");
|
||||
Path tempOutputDir = tempDir.resolve("output");
|
||||
Path tempImagesDir = tempDir.resolve("images");
|
||||
Path finalOutputFile = tempDir.resolve("final_output.pdf");
|
||||
if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
|
||||
throw new IOException("ocrRenderType wrong");
|
||||
}
|
||||
|
||||
Files.createDirectories(tempOutputDir);
|
||||
Files.createDirectories(tempImagesDir);
|
||||
// Get available Tesseract languages
|
||||
List<String> availableLanguages = getAvailableTesseractLanguages();
|
||||
|
||||
// Validate selected languages
|
||||
selectedLanguages =
|
||||
selectedLanguages.stream().filter(availableLanguages::contains).toList();
|
||||
|
||||
if (selectedLanguages.isEmpty()) {
|
||||
throw new IOException("None of the selected languages are valid.");
|
||||
}
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
Path sidecarTextPath = null;
|
||||
|
||||
try {
|
||||
// Save input file
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
PDFMergerUtility merger = new PDFMergerUtility();
|
||||
merger.setDestinationFileName(finalOutputFile.toString());
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) {
|
||||
PDFRenderer pdfRenderer = new PDFRenderer(document);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
// Run OCR Command
|
||||
String languageOption = String.join("+", selectedLanguages);
|
||||
|
||||
for (int pageNum = 0; pageNum < pageCount; pageNum++) {
|
||||
PDPage page = document.getPage(pageNum);
|
||||
boolean hasText = false;
|
||||
List<String> command =
|
||||
new ArrayList<>(
|
||||
Arrays.asList(
|
||||
"ocrmypdf",
|
||||
"--verbose",
|
||||
"2",
|
||||
"--output-type",
|
||||
"pdf",
|
||||
"--pdf-renderer",
|
||||
ocrRenderType));
|
||||
|
||||
// Check for existing text
|
||||
try (PDDocument tempDoc = new PDDocument()) {
|
||||
tempDoc.addPage(page);
|
||||
PDFTextStripper stripper = new PDFTextStripper();
|
||||
hasText = !stripper.getText(tempDoc).trim().isEmpty();
|
||||
}
|
||||
if (sidecar != null && sidecar) {
|
||||
sidecarTextPath = Files.createTempFile("sidecar", ".txt");
|
||||
command.add("--sidecar");
|
||||
command.add(sidecarTextPath.toString());
|
||||
}
|
||||
|
||||
boolean shouldOcr =
|
||||
switch (ocrType) {
|
||||
case "skip-text" -> !hasText;
|
||||
case "force-ocr" -> true;
|
||||
default -> true;
|
||||
};
|
||||
if (deskew != null && deskew) {
|
||||
command.add("--deskew");
|
||||
}
|
||||
if (clean != null && clean) {
|
||||
command.add("--clean");
|
||||
}
|
||||
if (cleanFinal != null && cleanFinal) {
|
||||
command.add("--clean-final");
|
||||
}
|
||||
if (ocrType != null && !"".equals(ocrType)) {
|
||||
if ("skip-text".equals(ocrType)) {
|
||||
command.add("--skip-text");
|
||||
} else if ("force-ocr".equals(ocrType)) {
|
||||
command.add("--force-ocr");
|
||||
} else if ("Normal".equals(ocrType)) {
|
||||
|
||||
Path pageOutputPath =
|
||||
tempOutputDir.resolve(String.format("page_%d.pdf", pageNum));
|
||||
|
||||
if (shouldOcr) {
|
||||
// Convert page to image
|
||||
BufferedImage image = pdfRenderer.renderImageWithDPI(pageNum, 300);
|
||||
Path imagePath =
|
||||
tempImagesDir.resolve(String.format("page_%d.png", pageNum));
|
||||
ImageIO.write(image, "png", imagePath.toFile());
|
||||
|
||||
// Build OCR command
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("tesseract");
|
||||
command.add(imagePath.toString());
|
||||
command.add(
|
||||
tempOutputDir
|
||||
.resolve(String.format("page_%d", pageNum))
|
||||
.toString());
|
||||
command.add("-l");
|
||||
command.add(String.join("+", languages));
|
||||
command.add("pdf"); // Always output PDF
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
Process process = pb.start();
|
||||
|
||||
// Capture any error output
|
||||
try (BufferedReader reader =
|
||||
new BufferedReader(
|
||||
new InputStreamReader(process.getErrorStream()))) {
|
||||
String line;
|
||||
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
||||
log.debug("Tesseract: {}", line);
|
||||
}
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException(
|
||||
"Tesseract failed with exit code: " + exitCode);
|
||||
}
|
||||
|
||||
// Add OCR'd PDF to merger
|
||||
merger.addSource(pageOutputPath.toFile());
|
||||
} else {
|
||||
// Save original page without OCR
|
||||
try (PDDocument pageDoc = new PDDocument()) {
|
||||
pageDoc.addPage(page);
|
||||
pageDoc.save(pageOutputPath.toFile());
|
||||
merger.addSource(pageOutputPath.toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Merge all pages into final PDF
|
||||
merger.mergeDocuments(null);
|
||||
command.addAll(
|
||||
Arrays.asList(
|
||||
"--language",
|
||||
languageOption,
|
||||
tempInputFile.toString(),
|
||||
tempOutputFile.toString()));
|
||||
|
||||
// Read the final PDF file
|
||||
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
|
||||
// Run CLI command
|
||||
ProcessExecutorResult result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
if (result.getRc() != 0
|
||||
&& result.getMessages().contains("multiprocessing/synchronize.py")
|
||||
&& result.getMessages()
|
||||
.contains("OSError: [Errno 38] Function not implemented")) {
|
||||
command.add("--jobs");
|
||||
command.add("1");
|
||||
result =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF)
|
||||
.runCommandWithOutputHandling(command);
|
||||
}
|
||||
|
||||
// Remove images from the OCR processed PDF if the flag is set to true
|
||||
if (removeImagesAfter != null && removeImagesAfter) {
|
||||
Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf");
|
||||
|
||||
List<String> gsCommand =
|
||||
Arrays.asList(
|
||||
"gs",
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dFILTERIMAGE",
|
||||
"-o",
|
||||
tempPdfWithoutImages.toString(),
|
||||
tempOutputFile.toString());
|
||||
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(gsCommand);
|
||||
tempOutputFile = tempPdfWithoutImages;
|
||||
}
|
||||
// Read the OCR processed PDF file
|
||||
byte[] pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
|
||||
|
||||
// Return the OCR processed PDF as a response
|
||||
String outputFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_OCR.pdf";
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(
|
||||
"Content-Disposition",
|
||||
"attachment; filename=\"" + outputFilename + "\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdfContent);
|
||||
if (sidecar != null && sidecar) {
|
||||
// Create a zip file containing both the PDF and the text file
|
||||
String outputZipFilename =
|
||||
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
|
||||
.replaceFirst("[.][^.]+$", "")
|
||||
+ "_OCR.zip";
|
||||
Path tempZipFile = Files.createTempFile("output_", ".zip");
|
||||
|
||||
} finally {
|
||||
// Clean up temporary files
|
||||
deleteDirectory(tempDir);
|
||||
}
|
||||
}
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
|
||||
// Add PDF file to the zip
|
||||
ZipEntry pdfEntry = new ZipEntry(outputFilename);
|
||||
zipOut.putNextEntry(pdfEntry);
|
||||
try (ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes)) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = pdfInputStream.read(buffer)) != -1) {
|
||||
zipOut.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
zipOut.closeEntry();
|
||||
|
||||
private void addFileToZip(File file, String filename, ZipOutputStream zipOut)
|
||||
throws IOException {
|
||||
if (!file.exists()) {
|
||||
log.warn("File {} does not exist, skipping", file);
|
||||
return;
|
||||
}
|
||||
// Add text file to the zip
|
||||
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
|
||||
zipOut.putNextEntry(txtEntry);
|
||||
Files.copy(sidecarTextPath, zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(file)) {
|
||||
ZipEntry zipEntry = new ZipEntry(filename);
|
||||
zipOut.putNextEntry(zipEntry);
|
||||
byte[] zipBytes = Files.readAllBytes(tempZipFile);
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = fis.read(buffer)) >= 0) {
|
||||
zipOut.write(buffer, 0, length);
|
||||
// Clean up the temporary zip file
|
||||
Files.deleteIfExists(tempZipFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
Files.deleteIfExists(sidecarTextPath);
|
||||
|
||||
// Return the zip file containing both the PDF and the text file
|
||||
return WebResponseUtils.bytesToWebResponse(
|
||||
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
|
||||
} else {
|
||||
// Return the OCR processed PDF as a response
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
|
||||
}
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
// Comment out as transferTo makes multipart handle cleanup
|
||||
// Files.deleteIfExists(tempInputFile);
|
||||
if (sidecarTextPath != null) {
|
||||
Files.deleteIfExists(sidecarTextPath);
|
||||
}
|
||||
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteDirectory(Path directory) {
|
||||
try {
|
||||
Files.walk(directory)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(
|
||||
path -> {
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (IOException e) {
|
||||
log.error("Error deleting {}: {}", path, e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.error("Error walking directory {}: {}", directory, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,29 +44,30 @@ public class RepairController {
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
description =
|
||||
"This endpoint repairs a given PDF file by running qpdf command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
"This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO")
|
||||
public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
|
||||
throws IOException, InterruptedException {
|
||||
MultipartFile inputFile = request.getFileInput();
|
||||
// Save the uploaded file to a temporary location
|
||||
Path tempInputFile = Files.createTempFile("input_", ".pdf");
|
||||
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
|
||||
byte[] pdfBytes = null;
|
||||
inputFile.transferTo(tempInputFile.toFile());
|
||||
try {
|
||||
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("qpdf");
|
||||
command.add("--replace-input"); // Automatically fixes problems it can
|
||||
command.add("--qdf"); // Linearizes and normalizes PDF structure
|
||||
command.add("--object-streams=disable"); // Can help with some corruptions
|
||||
command.add("gs");
|
||||
command.add("-o");
|
||||
command.add(tempOutputFile.toString());
|
||||
command.add("-sDEVICE=pdfwrite");
|
||||
command.add(tempInputFile.toString());
|
||||
|
||||
ProcessExecutorResult returnCode =
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
|
||||
ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
|
||||
.runCommandWithOutputHandling(command);
|
||||
|
||||
// Read the optimized PDF file
|
||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
|
||||
pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
|
||||
|
||||
// Return the optimized PDF as a response
|
||||
String outputFilename =
|
||||
@@ -77,6 +78,7 @@ public class RepairController {
|
||||
} finally {
|
||||
// Clean up the temporary files
|
||||
Files.deleteIfExists(tempInputFile);
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,22 +229,10 @@ public class StampController {
|
||||
calculatePositionY(
|
||||
pageSize, position, calculateTextCapHeight(font, fontSize), margin);
|
||||
}
|
||||
// Split the stampText into multiple lines
|
||||
String[] lines = stampText.split("\\\\n");
|
||||
|
||||
// Calculate dynamic line height based on font ascent and descent
|
||||
float ascent = font.getFontDescriptor().getAscent();
|
||||
float descent = font.getFontDescriptor().getDescent();
|
||||
float lineHeight = ((ascent - descent) / 1000) * fontSize;
|
||||
|
||||
contentStream.beginText();
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
// Set the text matrix for each line with rotation
|
||||
contentStream.setTextMatrix(
|
||||
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
|
||||
contentStream.showText(line);
|
||||
}
|
||||
contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y));
|
||||
contentStream.showText(stampText);
|
||||
contentStream.endText();
|
||||
}
|
||||
|
||||
|
||||
@@ -322,14 +322,27 @@ public class GetInfoOnPDF {
|
||||
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
|
||||
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
|
||||
encryption.put("KeyLength", pdfEncryption.getLength());
|
||||
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
||||
if (ap != null) {
|
||||
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||
|
||||
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
|
||||
permissionsNode.put("CanExtractContent", ap.canExtractContent());
|
||||
permissionsNode.put(
|
||||
"CanExtractForAccessibility", ap.canExtractForAccessibility());
|
||||
permissionsNode.put("CanFillInForm", ap.canFillInForm());
|
||||
permissionsNode.put("CanModify", ap.canModify());
|
||||
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
|
||||
permissionsNode.put("CanPrint", ap.canPrint());
|
||||
|
||||
encryption.set(
|
||||
"Permissions", permissionsNode); // set the node under "Permissions"
|
||||
}
|
||||
// Add other encryption-related properties as needed
|
||||
} else {
|
||||
encryption.put("IsEncrypted", false);
|
||||
}
|
||||
|
||||
ObjectNode permissionsNode = objectMapper.createObjectNode();
|
||||
setNodePermissions(pdfBoxDoc, permissionsNode);
|
||||
|
||||
ObjectNode pageInfoParent = objectMapper.createObjectNode();
|
||||
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
|
||||
ObjectNode pageInfo = objectMapper.createObjectNode();
|
||||
@@ -571,7 +584,6 @@ public class GetInfoOnPDF {
|
||||
jsonOutput.set("DocumentInfo", docInfoNode);
|
||||
jsonOutput.set("Compliancy", compliancy);
|
||||
jsonOutput.set("Encryption", encryption);
|
||||
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
|
||||
jsonOutput.set("Other", other);
|
||||
jsonOutput.set("PerPageInfo", pageInfoParent);
|
||||
|
||||
@@ -590,22 +602,6 @@ public class GetInfoOnPDF {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) {
|
||||
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
|
||||
|
||||
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
|
||||
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
|
||||
permissionsNode.put("Extracting for accessibility", getPermissionState(ap.canExtractForAccessibility()));
|
||||
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
|
||||
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
|
||||
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));
|
||||
permissionsNode.put("Printing", getPermissionState(ap.canPrint()));
|
||||
}
|
||||
|
||||
private String getPermissionState(boolean state) {
|
||||
return state ? "Allowed" : "Not Allowed";
|
||||
}
|
||||
|
||||
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
|
||||
if (outline == null) return;
|
||||
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
package stirling.software.SPDF.controller.api.security;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cms.CMSProcessable;
|
||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||
import org.bouncycastle.cms.CMSSignedData;
|
||||
import org.bouncycastle.cms.SignerInformation;
|
||||
import org.bouncycastle.cms.SignerInformationStore;
|
||||
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
|
||||
import org.bouncycastle.util.Store;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationRequest;
|
||||
import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||
import stirling.software.SPDF.service.CertificateValidationService;
|
||||
import stirling.software.SPDF.service.CustomPDDocumentFactory;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/security")
|
||||
@Tag(name = "Security", description = "Security APIs")
|
||||
public class ValidateSignatureController {
|
||||
|
||||
private final CustomPDDocumentFactory pdfDocumentFactory;
|
||||
private final CertificateValidationService certValidationService;
|
||||
|
||||
@Autowired
|
||||
public ValidateSignatureController(
|
||||
CustomPDDocumentFactory pdfDocumentFactory,
|
||||
CertificateValidationService certValidationService) {
|
||||
this.pdfDocumentFactory = pdfDocumentFactory;
|
||||
this.certValidationService = certValidationService;
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Validate PDF Digital Signature",
|
||||
description =
|
||||
"Validates the digital signatures in a PDF file against default or custom certificates. Input:PDF Output:JSON Type:SISO")
|
||||
@PostMapping(value = "/validate-signature")
|
||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||
List<SignatureValidationResult> results = new ArrayList<>();
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
// Load custom certificate if provided
|
||||
X509Certificate customCert = null;
|
||||
if (request.getCertFile() != null && !request.getCertFile().isEmpty()) {
|
||||
try (ByteArrayInputStream certStream =
|
||||
new ByteArrayInputStream(request.getCertFile().getBytes())) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
customCert = (X509Certificate) cf.generateCertificate(certStream);
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("Invalid certificate file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(file.getInputStream())) {
|
||||
List<PDSignature> signatures = document.getSignatureDictionaries();
|
||||
|
||||
for (PDSignature sig : signatures) {
|
||||
SignatureValidationResult result = new SignatureValidationResult();
|
||||
|
||||
try {
|
||||
byte[] signedContent = sig.getSignedContent(file.getInputStream());
|
||||
byte[] signatureBytes = sig.getContents(file.getInputStream());
|
||||
|
||||
CMSProcessable content = new CMSProcessableByteArray(signedContent);
|
||||
CMSSignedData signedData = new CMSSignedData(content, signatureBytes);
|
||||
|
||||
Store<X509CertificateHolder> certStore = signedData.getCertificates();
|
||||
SignerInformationStore signerStore = signedData.getSignerInfos();
|
||||
|
||||
for (SignerInformation signer : signerStore.getSigners()) {
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next();
|
||||
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
|
||||
|
||||
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
|
||||
result.setValid(isValid);
|
||||
|
||||
// Additional validations
|
||||
result.setChainValid(customCert != null
|
||||
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert)
|
||||
: certValidationService.validateCertificateChain(cert));
|
||||
|
||||
result.setTrustValid(customCert != null
|
||||
? certValidationService.validateTrustWithCustomCert(cert, customCert)
|
||||
: certValidationService.validateTrustStore(cert));
|
||||
|
||||
result.setNotRevoked(!certValidationService.isRevoked(cert));
|
||||
result.setNotExpired(!cert.getNotAfter().before(new Date()));
|
||||
|
||||
// Set basic signature info
|
||||
result.setSignerName(sig.getName());
|
||||
result.setSignatureDate(sig.getSignDate().getTime().toString());
|
||||
result.setReason(sig.getReason());
|
||||
result.setLocation(sig.getLocation());
|
||||
|
||||
// Set new certificate details
|
||||
result.setIssuerDN(cert.getIssuerX500Principal().getName());
|
||||
result.setSubjectDN(cert.getSubjectX500Principal().getName());
|
||||
result.setSerialNumber(cert.getSerialNumber().toString(16)); // Hex format
|
||||
result.setValidFrom(cert.getNotBefore().toString());
|
||||
result.setValidUntil(cert.getNotAfter().toString());
|
||||
result.setSignatureAlgorithm(cert.getSigAlgName());
|
||||
|
||||
// Get key size (if possible)
|
||||
try {
|
||||
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
|
||||
} catch (Exception e) {
|
||||
// If not RSA or error, set to 0
|
||||
result.setKeySize(0);
|
||||
}
|
||||
|
||||
result.setVersion(String.valueOf(cert.getVersion()));
|
||||
|
||||
// Set key usage
|
||||
List<String> keyUsages = new ArrayList<>();
|
||||
boolean[] keyUsageFlags = cert.getKeyUsage();
|
||||
if (keyUsageFlags != null) {
|
||||
String[] keyUsageLabels = {
|
||||
"Digital Signature", "Non-Repudiation", "Key Encipherment",
|
||||
"Data Encipherment", "Key Agreement", "Certificate Signing",
|
||||
"CRL Signing", "Encipher Only", "Decipher Only"
|
||||
};
|
||||
for (int i = 0; i < keyUsageFlags.length; i++) {
|
||||
if (keyUsageFlags[i]) {
|
||||
keyUsages.add(keyUsageLabels[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.setKeyUsages(keyUsages);
|
||||
|
||||
// Check if self-signed
|
||||
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
result.setValid(false);
|
||||
result.setErrorMessage("Signature validation failed: " + e.getMessage());
|
||||
}
|
||||
|
||||
results.add(result);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(results);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,6 @@ public class WatermarkController {
|
||||
float opacity = request.getOpacity();
|
||||
int widthSpacer = request.getWidthSpacer();
|
||||
int heightSpacer = request.getHeightSpacer();
|
||||
String customColor = request.getCustomColor();
|
||||
boolean convertPdfToImage = request.isConvertPDFToImage();
|
||||
|
||||
// Load the input PDF
|
||||
@@ -98,8 +97,7 @@ public class WatermarkController {
|
||||
widthSpacer,
|
||||
heightSpacer,
|
||||
fontSize,
|
||||
alphabet,
|
||||
customColor);
|
||||
alphabet);
|
||||
} else if ("image".equalsIgnoreCase(watermarkType)) {
|
||||
addImageWatermark(
|
||||
contentStream,
|
||||
@@ -138,8 +136,7 @@ public class WatermarkController {
|
||||
int widthSpacer,
|
||||
int heightSpacer,
|
||||
float fontSize,
|
||||
String alphabet,
|
||||
String colorString)
|
||||
String alphabet)
|
||||
throws IOException {
|
||||
String resourceDir = "";
|
||||
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
|
||||
@@ -176,18 +173,7 @@ public class WatermarkController {
|
||||
}
|
||||
|
||||
contentStream.setFont(font, fontSize);
|
||||
|
||||
Color redactColor;
|
||||
try {
|
||||
if (!colorString.startsWith("#")) {
|
||||
colorString = "#" + colorString;
|
||||
}
|
||||
redactColor = Color.decode(colorString);
|
||||
} catch (NumberFormatException e) {
|
||||
|
||||
redactColor = Color.LIGHT_GRAY;
|
||||
}
|
||||
contentStream.setNonStrokingColor(redactColor);
|
||||
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
|
||||
|
||||
String[] textLines = watermarkText.split("\\\\n");
|
||||
float maxLineWidth = 0;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
package stirling.software.SPDF.controller.api.strippers;
|
||||
|
||||
import java.awt.Shape;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.fontbox.util.BoundingBox;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType3Font;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.apache.pdfbox.text.PDFTextStripperByArea;
|
||||
import org.apache.pdfbox.text.TextPosition;
|
||||
|
||||
/**
|
||||
* Class to extract tabular data from a PDF. Works by making a first pass of the page to group all
|
||||
* nearby text items together, and then inferring a 2D grid from these regions. Each table cell is
|
||||
* then extracted using a PDFTextStripperByArea object.
|
||||
*
|
||||
* <p>Works best when headers are included in the detected region, to ensure representative text in
|
||||
* every column.
|
||||
*
|
||||
* <p>Based upon DrawPrintTextLocations PDFBox example
|
||||
* (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java)
|
||||
*
|
||||
* @author Beldaz
|
||||
*/
|
||||
public class PDFTableStripper extends PDFTextStripper {
|
||||
|
||||
/**
|
||||
* This will print the documents data, for each table cell.
|
||||
*
|
||||
* @param args The command line arguments.
|
||||
* @throws IOException If there is an error parsing the document.
|
||||
*/
|
||||
/*
|
||||
* Used in methods derived from DrawPrintTextLocations
|
||||
*/
|
||||
private AffineTransform flipAT;
|
||||
|
||||
private AffineTransform rotateAT;
|
||||
|
||||
/** Regions updated by calls to writeString */
|
||||
private Set<Rectangle2D> boxes;
|
||||
|
||||
// Border to allow when finding intersections
|
||||
private double dx = 1.0; // This value works for me, feel free to tweak (or add setter)
|
||||
private double dy = 0.000; // Rows of text tend to overlap, so need to extend
|
||||
|
||||
/** Region in which to find table (otherwise whole page) */
|
||||
private Rectangle2D regionArea;
|
||||
|
||||
/** Number of rows in inferred table */
|
||||
private int nRows = 0;
|
||||
|
||||
/** Number of columns in inferred table */
|
||||
private int nCols = 0;
|
||||
|
||||
/** This is the object that does the text extraction */
|
||||
private PDFTextStripperByArea regionStripper;
|
||||
|
||||
/**
|
||||
* 1D intervals - used for calculateTableRegions()
|
||||
*
|
||||
* @author Beldaz
|
||||
*/
|
||||
public static class Interval {
|
||||
double start;
|
||||
double end;
|
||||
|
||||
public Interval(double start, double end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public void add(Interval col) {
|
||||
if (col.start < start) start = col.start;
|
||||
if (col.end > end) end = col.end;
|
||||
}
|
||||
|
||||
public static void addTo(Interval x, LinkedList<Interval> columns) {
|
||||
int p = 0;
|
||||
Iterator<Interval> it = columns.iterator();
|
||||
// Find where x should go
|
||||
while (it.hasNext()) {
|
||||
Interval col = it.next();
|
||||
if (x.end >= col.start) {
|
||||
if (x.start <= col.end) { // overlaps
|
||||
x.add(col);
|
||||
it.remove();
|
||||
}
|
||||
break;
|
||||
}
|
||||
++p;
|
||||
}
|
||||
while (it.hasNext()) {
|
||||
Interval col = it.next();
|
||||
if (x.start > col.end) break;
|
||||
x.add(col);
|
||||
it.remove();
|
||||
}
|
||||
columns.add(p, x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new PDFTableStripper object.
|
||||
*
|
||||
* @throws IOException If there is an error loading the properties.
|
||||
*/
|
||||
public PDFTableStripper() throws IOException {
|
||||
super.setShouldSeparateByBeads(false);
|
||||
regionStripper = new PDFTextStripperByArea();
|
||||
regionStripper.setSortByPosition(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the region to group text by.
|
||||
*
|
||||
* @param rect The rectangle area to retrieve the text from.
|
||||
*/
|
||||
public void setRegion(Rectangle2D rect) {
|
||||
regionArea = rect;
|
||||
}
|
||||
|
||||
public int getRows() {
|
||||
return nRows;
|
||||
}
|
||||
|
||||
public int getColumns() {
|
||||
return nCols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text for the region, this should be called after extractTable().
|
||||
*
|
||||
* @return The text that was identified in that region.
|
||||
*/
|
||||
public String getText(int row, int col) {
|
||||
return regionStripper.getTextForRegion("el" + col + "x" + row);
|
||||
}
|
||||
|
||||
public void extractTable(PDPage pdPage) throws IOException {
|
||||
setStartPage(getCurrentPageNo());
|
||||
setEndPage(getCurrentPageNo());
|
||||
|
||||
boxes = new HashSet<Rectangle2D>();
|
||||
// flip y-axis
|
||||
flipAT = new AffineTransform();
|
||||
flipAT.translate(0, pdPage.getBBox().getHeight());
|
||||
flipAT.scale(1, -1);
|
||||
|
||||
// page may be rotated
|
||||
rotateAT = new AffineTransform();
|
||||
int rotation = pdPage.getRotation();
|
||||
if (rotation != 0) {
|
||||
PDRectangle mediaBox = pdPage.getMediaBox();
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
rotateAT.translate(mediaBox.getHeight(), 0);
|
||||
break;
|
||||
case 270:
|
||||
rotateAT.translate(0, mediaBox.getWidth());
|
||||
break;
|
||||
case 180:
|
||||
rotateAT.translate(mediaBox.getWidth(), mediaBox.getHeight());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
rotateAT.rotate(Math.toRadians(rotation));
|
||||
}
|
||||
// Trigger processing of the document so that writeString is called.
|
||||
try (Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream())) {
|
||||
super.output = dummy;
|
||||
super.processPage(pdPage);
|
||||
}
|
||||
|
||||
Rectangle2D[][] regions = calculateTableRegions();
|
||||
|
||||
// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + "
|
||||
// regions");
|
||||
for (int i = 0; i < nCols; ++i) {
|
||||
for (int j = 0; j < nRows; ++j) {
|
||||
final Rectangle2D region = regions[i][j];
|
||||
regionStripper.addRegion("el" + i + "x" + j, region);
|
||||
}
|
||||
}
|
||||
|
||||
regionStripper.extractRegions(pdPage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer a rectangular grid of regions from the boxes field.
|
||||
*
|
||||
* @return 2D array of table regions (as Rectangle2D objects). Note that some of these regions
|
||||
* may have no content.
|
||||
*/
|
||||
private Rectangle2D[][] calculateTableRegions() {
|
||||
|
||||
// Build up a list of all table regions, based upon the populated
|
||||
// regions of boxes field. Treats the horizontal and vertical extents
|
||||
// of each box as distinct
|
||||
LinkedList<Interval> columns = new LinkedList<Interval>();
|
||||
LinkedList<Interval> rows = new LinkedList<Interval>();
|
||||
|
||||
for (Rectangle2D box : boxes) {
|
||||
Interval x = new Interval(box.getMinX(), box.getMaxX());
|
||||
Interval y = new Interval(box.getMinY(), box.getMaxY());
|
||||
|
||||
Interval.addTo(x, columns);
|
||||
Interval.addTo(y, rows);
|
||||
}
|
||||
|
||||
nRows = rows.size();
|
||||
nCols = columns.size();
|
||||
Rectangle2D[][] regions = new Rectangle2D[nCols][nRows];
|
||||
int i = 0;
|
||||
// Label regions from top left, rather than the transformed orientation
|
||||
for (Interval column : columns) {
|
||||
int j = 0;
|
||||
for (Interval row : rows) {
|
||||
regions[nCols - i - 1][nRows - j - 1] =
|
||||
new Rectangle2D.Double(
|
||||
column.start,
|
||||
row.start,
|
||||
column.end - column.start,
|
||||
row.end - row.start);
|
||||
++j;
|
||||
}
|
||||
++i;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register each character's bounding box, updating boxes field to maintain a list of all
|
||||
* distinct groups of characters.
|
||||
*
|
||||
* <p>Overrides the default functionality of PDFTextStripper. Most of this is taken from
|
||||
* DrawPrintTextLocations.java, with extra steps at end of main loop
|
||||
*/
|
||||
@Override
|
||||
protected void writeString(String string, List<TextPosition> textPositions) throws IOException {
|
||||
for (TextPosition text : textPositions) {
|
||||
// glyph space -> user space
|
||||
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
|
||||
AffineTransform at = text.getTextMatrix().createAffineTransform();
|
||||
PDFont font = text.getFont();
|
||||
BoundingBox bbox = font.getBoundingBox();
|
||||
|
||||
// advance width, bbox height (glyph space)
|
||||
float xadvance =
|
||||
font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars
|
||||
Rectangle2D.Float rect =
|
||||
new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
|
||||
|
||||
if (font instanceof PDType3Font) {
|
||||
// bbox and font matrix are unscaled
|
||||
at.concatenate(font.getFontMatrix().createAffineTransform());
|
||||
} else {
|
||||
// bbox and font matrix are already scaled to 1000
|
||||
at.scale(1 / 1000f, 1 / 1000f);
|
||||
}
|
||||
Shape s = at.createTransformedShape(rect);
|
||||
s = flipAT.createTransformedShape(s);
|
||||
s = rotateAT.createTransformedShape(s);
|
||||
|
||||
//
|
||||
// Merge character's bounding box with boxes field
|
||||
//
|
||||
Rectangle2D bounds = s.getBounds2D();
|
||||
// Pad sides to detect almost touching boxes
|
||||
Rectangle2D hitbox = bounds.getBounds2D();
|
||||
hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy);
|
||||
hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy);
|
||||
|
||||
// Find all overlapping boxes
|
||||
List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>();
|
||||
for (Rectangle2D box : boxes) {
|
||||
if (box.intersects(hitbox)) {
|
||||
intersectList.add(box);
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all touching boxes and update
|
||||
// (NOTE: Potentially this could leave some overlapping boxes un-merged,
|
||||
// but it's sufficient for now and get's fixed up in calculateTableRegions)
|
||||
for (Rectangle2D box : intersectList) {
|
||||
bounds.add(box);
|
||||
boxes.remove(box);
|
||||
}
|
||||
boxes.add(bounds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method does nothing in this derived class, because beads and regions are incompatible.
|
||||
* Beads are ignored when stripping by area.
|
||||
*
|
||||
* @param aShouldSeparateByBeads The new grouping of beads.
|
||||
*/
|
||||
@Override
|
||||
public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {}
|
||||
|
||||
/** Adapted from PDFTextStripperByArea {@inheritDoc} */
|
||||
@Override
|
||||
protected void processTextPosition(TextPosition text) {
|
||||
if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) {
|
||||
// skip character
|
||||
} else {
|
||||
super.processTextPosition(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,9 +34,7 @@ public class DatabaseWebController {
|
||||
}
|
||||
|
||||
List<FileInfo> backupList = databaseBackupHelper.getBackupList();
|
||||
model.addAttribute("backupFiles", backupList);
|
||||
|
||||
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
|
||||
model.addAttribute("systemUpdate", backupList);
|
||||
|
||||
return "database";
|
||||
}
|
||||
|
||||
@@ -55,11 +55,6 @@ public class HomeWebController {
|
||||
return "licenses";
|
||||
}
|
||||
|
||||
@GetMapping("/releases")
|
||||
public String getReleaseNotes(Model model) {
|
||||
return "releases";
|
||||
}
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("currentPage", "home");
|
||||
|
||||
@@ -53,13 +53,6 @@ public class SecurityWebController {
|
||||
return "security/cert-sign";
|
||||
}
|
||||
|
||||
@GetMapping("/validate-signature")
|
||||
@Hidden
|
||||
public String certSignVerifyForm(Model model) {
|
||||
model.addAttribute("currentPage", "validate-signature");
|
||||
return "security/validate-signature";
|
||||
}
|
||||
|
||||
@GetMapping("/remove-cert-sign")
|
||||
@Hidden
|
||||
public String certUnSignForm(Model model) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.springframework.core.annotation.Order;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
@@ -30,6 +31,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
|
||||
import stirling.software.SPDF.model.provider.GoogleProvider;
|
||||
import stirling.software.SPDF.model.provider.KeycloakProvider;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
import stirling.software.SPDF.utils.GeneralUtils;
|
||||
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "")
|
||||
@@ -134,44 +136,20 @@ public class ApplicationProperties {
|
||||
private String privateKey;
|
||||
private String spCert;
|
||||
|
||||
public InputStream getIdpMetadataUri() throws IOException {
|
||||
if (idpMetadataUri.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
|
||||
.getInputStream();
|
||||
}
|
||||
try {
|
||||
URI uri = new URI(idpMetadataUri);
|
||||
URL url = uri.toURL();
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
return connection.getInputStream();
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
|
||||
}
|
||||
public Resource getIdpMetadataUri() throws IOException {
|
||||
return GeneralUtils.filePathToResource(idpMetadataUri);
|
||||
}
|
||||
|
||||
public Resource getSpCert() {
|
||||
if (spCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(spCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(spCert);
|
||||
}
|
||||
return GeneralUtils.filePathToResource(spCert);
|
||||
}
|
||||
|
||||
public Resource getidpCert() {
|
||||
if (idpCert.startsWith("classpath:")) {
|
||||
return new ClassPathResource(idpCert.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(idpCert);
|
||||
}
|
||||
return GeneralUtils.filePathToResource(idpCert);
|
||||
}
|
||||
|
||||
public Resource getPrivateKey() {
|
||||
if (privateKey.startsWith("classpath:")) {
|
||||
return new ClassPathResource(privateKey.substring("classpath:".length()));
|
||||
} else {
|
||||
return new FileSystemResource(privateKey);
|
||||
}
|
||||
return GeneralUtils.filePathToResource(privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,20 +298,12 @@ public class ApplicationProperties {
|
||||
public static class SessionLimit {
|
||||
private int libreOfficeSessionLimit;
|
||||
private int pdfToHtmlSessionLimit;
|
||||
private int ocrMyPdfSessionLimit;
|
||||
private int pythonOpenCvSessionLimit;
|
||||
private int ghostScriptSessionLimit;
|
||||
private int weasyPrintSessionLimit;
|
||||
private int installAppSessionLimit;
|
||||
private int calibreSessionLimit;
|
||||
private int qpdfSessionLimit;
|
||||
private int tesseractSessionLimit;
|
||||
|
||||
public int getQpdfSessionLimit() {
|
||||
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
|
||||
}
|
||||
|
||||
public int getTesseractSessionLimit() {
|
||||
return tesseractSessionLimit > 0 ? tesseractSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getLibreOfficeSessionLimit() {
|
||||
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
|
||||
@@ -343,10 +313,18 @@ public class ApplicationProperties {
|
||||
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
|
||||
}
|
||||
|
||||
public int getOcrMyPdfSessionLimit() {
|
||||
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
|
||||
}
|
||||
|
||||
public int getPythonOpenCvSessionLimit() {
|
||||
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
|
||||
}
|
||||
|
||||
public int getGhostScriptSessionLimit() {
|
||||
return ghostScriptSessionLimit > 0 ? ghostScriptSessionLimit : 16;
|
||||
}
|
||||
|
||||
public int getWeasyPrintSessionLimit() {
|
||||
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
|
||||
}
|
||||
@@ -364,20 +342,12 @@ public class ApplicationProperties {
|
||||
public static class TimeoutMinutes {
|
||||
private long libreOfficeTimeoutMinutes;
|
||||
private long pdfToHtmlTimeoutMinutes;
|
||||
private long ocrMyPdfTimeoutMinutes;
|
||||
private long pythonOpenCvTimeoutMinutes;
|
||||
private long ghostScriptTimeoutMinutes;
|
||||
private long weasyPrintTimeoutMinutes;
|
||||
private long installAppTimeoutMinutes;
|
||||
private long calibreTimeoutMinutes;
|
||||
private long tesseractTimeoutMinutes;
|
||||
private long qpdfTimeoutMinutes;
|
||||
|
||||
public long getTesseractTimeoutMinutes() {
|
||||
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getQpdfTimeoutMinutes() {
|
||||
return qpdfTimeoutMinutes > 0 ? qpdfTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getLibreOfficeTimeoutMinutes() {
|
||||
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
|
||||
@@ -387,10 +357,18 @@ public class ApplicationProperties {
|
||||
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
|
||||
}
|
||||
|
||||
public long getOcrMyPdfTimeoutMinutes() {
|
||||
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getPythonOpenCvTimeoutMinutes() {
|
||||
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getGhostScriptTimeoutMinutes() {
|
||||
return ghostScriptTimeoutMinutes > 0 ? ghostScriptTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
public long getWeasyPrintTimeoutMinutes() {
|
||||
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@ package stirling.software.SPDF.model;
|
||||
|
||||
public enum AuthenticationType {
|
||||
WEB,
|
||||
SSO
|
||||
OAUTH2
|
||||
}
|
||||
|
||||
@@ -18,15 +18,4 @@ public class OptimizePdfRequest extends PDFFile {
|
||||
|
||||
@Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.")
|
||||
private String expectedOutputSize;
|
||||
|
||||
@Schema(
|
||||
description = "Whether to linearize the PDF for faster web viewing. Default is false.",
|
||||
defaultValue = "false")
|
||||
private Boolean linearize = false;
|
||||
|
||||
@Schema(
|
||||
description =
|
||||
"Whether to normalize the PDF content for better compatibility. Default is true.",
|
||||
defaultValue = "true")
|
||||
private Boolean normalize = true;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,18 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
||||
@Schema(description = "List of languages to use in OCR processing")
|
||||
private List<String> languages;
|
||||
|
||||
@Schema(description = "Include OCR text in a sidecar text file if set to true")
|
||||
private boolean sidecar;
|
||||
|
||||
@Schema(description = "Deskew the input file if set to true")
|
||||
private boolean deskew;
|
||||
|
||||
@Schema(description = "Clean the input file if set to true")
|
||||
private boolean clean;
|
||||
|
||||
@Schema(description = "Clean the final output if set to true")
|
||||
private boolean cleanFinal;
|
||||
|
||||
@Schema(
|
||||
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
|
||||
allowableValues = {"skip-text", "force-ocr", "Normal"})
|
||||
@@ -25,4 +37,7 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
|
||||
allowableValues = {"hocr", "sandwich"},
|
||||
defaultValue = "hocr")
|
||||
private String ocrRenderType = "hocr";
|
||||
|
||||
@Schema(description = "Remove images from the output PDF if set to true")
|
||||
private boolean removeImagesAfter;
|
||||
}
|
||||
|
||||
@@ -45,9 +45,6 @@ public class AddWatermarkRequest extends PDFFile {
|
||||
@Schema(description = "The height spacer between watermark elements", example = "50")
|
||||
private int heightSpacer;
|
||||
|
||||
@Schema(description = "The color for watermark", defaultValue = "#d3d3d3")
|
||||
private String customColor = "#d3d3d3";
|
||||
|
||||
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
|
||||
private boolean convertPDFToImage;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package stirling.software.SPDF.model.api.security;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import stirling.software.SPDF.model.api.PDFFile;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SignatureValidationRequest extends PDFFile {
|
||||
|
||||
@Schema(description = "(Optional) file to compare PDF cert signatures against x.509 format")
|
||||
private MultipartFile certFile;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package stirling.software.SPDF.model.api.security;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SignatureValidationResult {
|
||||
private boolean valid;
|
||||
private String signerName;
|
||||
private String signatureDate;
|
||||
private String reason;
|
||||
private String location;
|
||||
private String errorMessage;
|
||||
private boolean chainValid;
|
||||
private boolean trustValid;
|
||||
private boolean notExpired;
|
||||
private boolean notRevoked;
|
||||
|
||||
private String issuerDN; // Certificate issuer's Distinguished Name
|
||||
private String subjectDN; // Certificate subject's Distinguished Name
|
||||
private String serialNumber; // Certificate serial number
|
||||
private String validFrom; // Certificate validity start date
|
||||
private String validUntil; // Certificate validity end date
|
||||
private String signatureAlgorithm;// Algorithm used for signing
|
||||
private int keySize; // Key size in bits
|
||||
private String version; // Certificate version
|
||||
private List<String> keyUsages; // List of key usage purposes
|
||||
private boolean isSelfSigned; // Whether the certificate is self-signed
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package stirling.software.SPDF.pdf;
|
||||
|
||||
import org.apache.commons.csv.CSVFormat;
|
||||
|
||||
import technology.tabula.writers.CSVWriter;
|
||||
|
||||
public class FlexibleCSVWriter extends CSVWriter {
|
||||
|
||||
public FlexibleCSVWriter() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FlexibleCSVWriter(CSVFormat csvFormat) {
|
||||
super(csvFormat);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package stirling.software.SPDF.repository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -20,6 +19,4 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Optional<User> findByUsername(String username);
|
||||
|
||||
Optional<User> findByApiKey(String apiKey);
|
||||
|
||||
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
|
||||
}
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
package stirling.software.SPDF.service;
|
||||
|
||||
import io.github.pixee.security.BoundedLineReader;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.CertPath;
|
||||
import java.security.cert.CertPathValidator;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.PKIXParameters;
|
||||
import java.security.cert.TrustAnchor;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
@Service
|
||||
public class CertificateValidationService {
|
||||
private KeyStore trustStore;
|
||||
|
||||
@PostConstruct
|
||||
private void initializeTrustStore() throws Exception {
|
||||
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
trustStore.load(null, null);
|
||||
loadMozillaCertificates();
|
||||
}
|
||||
|
||||
private void loadMozillaCertificates() throws Exception {
|
||||
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
|
||||
String line;
|
||||
StringBuilder certData = new StringBuilder();
|
||||
boolean inCert = false;
|
||||
int certCount = 0;
|
||||
|
||||
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
|
||||
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
|
||||
inCert = true;
|
||||
certData = new StringBuilder();
|
||||
continue;
|
||||
}
|
||||
if (inCert) {
|
||||
if ("END".equals(line)) {
|
||||
inCert = false;
|
||||
byte[] certBytes = parseOctalData(certData.toString());
|
||||
if (certBytes != null) {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert =
|
||||
(X509Certificate)
|
||||
cf.generateCertificate(
|
||||
new ByteArrayInputStream(certBytes));
|
||||
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
|
||||
}
|
||||
} else {
|
||||
certData.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] parseOctalData(String data) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
String[] tokens = data.split("\\\\");
|
||||
for (String token : tokens) {
|
||||
token = token.trim();
|
||||
if (!token.isEmpty()) {
|
||||
baos.write(Integer.parseInt(token, 8));
|
||||
}
|
||||
}
|
||||
return baos.toByteArray();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateCertificateChain(X509Certificate cert) {
|
||||
try {
|
||||
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X.509");
|
||||
List<X509Certificate> certList = Arrays.asList(cert);
|
||||
CertPath certPath = cf.generateCertPath(certList);
|
||||
|
||||
Set<TrustAnchor> anchors = new HashSet<>();
|
||||
Enumeration<String> aliases = trustStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||
if (trustCert instanceof X509Certificate) {
|
||||
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
|
||||
}
|
||||
}
|
||||
|
||||
PKIXParameters params = new PKIXParameters(anchors);
|
||||
params.setRevocationEnabled(false);
|
||||
validator.validate(certPath, params);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateTrustStore(X509Certificate cert) {
|
||||
try {
|
||||
Enumeration<String> aliases = trustStore.aliases();
|
||||
while (aliases.hasMoreElements()) {
|
||||
Object trustCert = trustStore.getCertificate(aliases.nextElement());
|
||||
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (KeyStoreException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isRevoked(X509Certificate cert) {
|
||||
try {
|
||||
cert.checkValidity();
|
||||
return false;
|
||||
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateCertificateChainWithCustomCert(
|
||||
X509Certificate cert, X509Certificate customCert) {
|
||||
try {
|
||||
cert.verify(customCert.getPublicKey());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
|
||||
try {
|
||||
// Compare the issuer of the signature certificate with the custom certificate
|
||||
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ public class MetricsAggregatorService {
|
||||
this.postHogService = postHogService;
|
||||
}
|
||||
|
||||
@Scheduled(fixedRate = 7200000) // Run every 2 hours
|
||||
@Scheduled(fixedRate = 900000) // Run every 15 minutes
|
||||
public void aggregateAndSendMetrics() {
|
||||
Map<String, Object> metrics = new HashMap<>();
|
||||
Search.in(meterRegistry)
|
||||
@@ -32,24 +32,11 @@ public class MetricsAggregatorService {
|
||||
.counters()
|
||||
.forEach(
|
||||
counter -> {
|
||||
String method = counter.getId().getTag("method");
|
||||
String uri = counter.getId().getTag("uri");
|
||||
|
||||
// Skip if either method or uri is null
|
||||
if (method == null || uri == null) {
|
||||
return;
|
||||
}
|
||||
if (!method.equals("GET") && !method.equals("POST")) {
|
||||
return;
|
||||
}
|
||||
// Skip URIs that are 2 characters or shorter
|
||||
if (uri.length() <= 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key =
|
||||
String.format(
|
||||
"http_requests_%s_%s", method, uri.replace("/", "_"));
|
||||
"http_requests_%s_%s",
|
||||
counter.getId().getTag("method"),
|
||||
counter.getId().getTag("uri").replace("/", "_"));
|
||||
|
||||
double currentCount = counter.count();
|
||||
double lastCount = lastSentMetrics.getOrDefault(key, 0.0);
|
||||
|
||||
@@ -17,18 +17,15 @@ public class PdfMetadataService {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final String stirlingPDFLabel;
|
||||
private final UserServiceInterface userService;
|
||||
private final boolean runningEE;
|
||||
|
||||
@Autowired
|
||||
public PdfMetadataService(
|
||||
ApplicationProperties applicationProperties,
|
||||
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
|
||||
@Qualifier("runningEE") boolean runningEE,
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.stirlingPDFLabel = stirlingPDFLabel;
|
||||
this.userService = userService;
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
|
||||
@@ -64,8 +61,10 @@ public class PdfMetadataService {
|
||||
|
||||
String creator = stirlingPDFLabel;
|
||||
|
||||
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||
&& runningEE) {
|
||||
if (applicationProperties
|
||||
.getEnterpriseEdition()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()) {
|
||||
|
||||
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
|
||||
pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
|
||||
@@ -84,8 +83,10 @@ public class PdfMetadataService {
|
||||
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
|
||||
|
||||
String author = pdfMetadata.getAuthor();
|
||||
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata()
|
||||
&& runningEE) {
|
||||
if (applicationProperties
|
||||
.getEnterpriseEdition()
|
||||
.getCustomMetadata()
|
||||
.isAutoUpdateMetadata()) {
|
||||
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
|
||||
|
||||
if (userService != null) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import java.util.TimeZone;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.posthog.java.PostHog;
|
||||
@@ -27,25 +26,19 @@ import stirling.software.SPDF.model.ApplicationProperties;
|
||||
public class PostHogService {
|
||||
private final PostHog postHog;
|
||||
private final String uniqueId;
|
||||
private final String appVersion;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final UserServiceInterface userService;
|
||||
private final Environment env;
|
||||
|
||||
@Autowired
|
||||
public PostHogService(
|
||||
PostHog postHog,
|
||||
@Qualifier("UUID") String uuid,
|
||||
@Qualifier("appVersion") String appVersion,
|
||||
ApplicationProperties applicationProperties,
|
||||
@Autowired(required = false) UserServiceInterface userService,
|
||||
Environment env) {
|
||||
@Autowired(required = false) UserServiceInterface userService) {
|
||||
this.postHog = postHog;
|
||||
this.uniqueId = uuid;
|
||||
this.appVersion = appVersion;
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.userService = userService;
|
||||
this.env = env;
|
||||
captureSystemInfo();
|
||||
}
|
||||
|
||||
@@ -71,16 +64,6 @@ public class PostHogService {
|
||||
Map<String, Object> metrics = new HashMap<>();
|
||||
|
||||
try {
|
||||
// Application version
|
||||
metrics.put("app_version", appVersion);
|
||||
String deploymentType = "JAR"; // default
|
||||
if ("true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"))) {
|
||||
deploymentType = "EXE";
|
||||
} else if (isRunningInDocker()) {
|
||||
deploymentType = "DOCKER";
|
||||
}
|
||||
metrics.put("deployment_type", deploymentType);
|
||||
|
||||
// System info
|
||||
metrics.put("os_name", System.getProperty("os.name"));
|
||||
metrics.put("os_version", System.getProperty("os.version"));
|
||||
@@ -149,6 +132,7 @@ public class PostHogService {
|
||||
|
||||
// Docker detection and stats
|
||||
boolean isDocker = isRunningInDocker();
|
||||
metrics.put("is_docker", isDocker);
|
||||
if (isDocker) {
|
||||
metrics.put("docker_metrics", getDockerMetrics());
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class FileToPdf {
|
||||
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
|
||||
Path filePath = tempUnzippedDir.resolve(entry.getName());
|
||||
if (!entry.isDirectory()) {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
if (entry.getName().toLowerCase().endsWith(".html")
|
||||
@@ -175,7 +175,7 @@ public class FileToPdf {
|
||||
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
|
||||
ZipEntry entry = zipIn.getNextEntry();
|
||||
while (entry != null) {
|
||||
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
|
||||
Path filePath = tempDirectory.resolve(entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
Files.createDirectories(filePath); // Explicitly create the directory structure
|
||||
} else {
|
||||
@@ -241,14 +241,4 @@ public class FileToPdf {
|
||||
Files.deleteIfExists(tempOutputFile);
|
||||
}
|
||||
}
|
||||
|
||||
static String sanitizeZipFilename(String entryName) {
|
||||
if (entryName == null || entryName.trim().isEmpty()) {
|
||||
return entryName;
|
||||
}
|
||||
while (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
entryName = entryName.replace("../", "").replace("..\\", "");
|
||||
}
|
||||
return entryName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
|
||||
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.fathzer.soft.javaluator.DoubleEvaluator;
|
||||
@@ -349,4 +353,23 @@ public class GeneralUtils {
|
||||
return "GenericID";
|
||||
}
|
||||
}
|
||||
|
||||
public static Resource filePathToResource(String resourceFile) {
|
||||
if (resourceFile == null) {
|
||||
throw new IllegalStateException("file is not configured");
|
||||
}
|
||||
|
||||
if (resourceFile.startsWith("classpath:")) {
|
||||
return new ClassPathResource(resourceFile.substring("classpath:".length()));
|
||||
} else if (resourceFile.startsWith("http://") || resourceFile.startsWith("https://")) {
|
||||
try {
|
||||
return new UrlResource(resourceFile);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to create URL resource: " + resourceFile, e);
|
||||
}
|
||||
} else {
|
||||
return new FileSystemResource(resourceFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ public class ProcessExecutor {
|
||||
public enum Processes {
|
||||
LIBRE_OFFICE,
|
||||
PDFTOHTML,
|
||||
OCR_MY_PDF,
|
||||
PYTHON_OPENCV,
|
||||
GHOSTSCRIPT,
|
||||
WEASYPRINT,
|
||||
INSTALL_APP,
|
||||
CALIBRE,
|
||||
TESSERACT,
|
||||
QPDF
|
||||
CALIBRE
|
||||
}
|
||||
|
||||
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
|
||||
@@ -59,11 +59,21 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getPdfToHtmlSessionLimit();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getOcrMyPdfSessionLimit();
|
||||
case PYTHON_OPENCV ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getPythonOpenCvSessionLimit();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getGhostScriptSessionLimit();
|
||||
case WEASYPRINT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -74,16 +84,6 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getInstallAppSessionLimit();
|
||||
case TESSERACT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getTesseractSessionLimit();
|
||||
case QPDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getSessionLimit()
|
||||
.getQpdfSessionLimit();
|
||||
case CALIBRE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -103,11 +103,21 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getPdfToHtmlTimeoutMinutes();
|
||||
case OCR_MY_PDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getOcrMyPdfTimeoutMinutes();
|
||||
case PYTHON_OPENCV ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getPythonOpenCvTimeoutMinutes();
|
||||
case GHOSTSCRIPT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getGhostScriptTimeoutMinutes();
|
||||
case WEASYPRINT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
@@ -118,16 +128,6 @@ public class ProcessExecutor {
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getInstallAppTimeoutMinutes();
|
||||
case TESSERACT ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getTesseractTimeoutMinutes();
|
||||
case QPDF ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
.getTimeoutMinutes()
|
||||
.getQpdfTimeoutMinutes();
|
||||
case CALIBRE ->
|
||||
applicationProperties
|
||||
.getProcessExecutor()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package stirling.software.SPDF.utils.propertyeditor;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class StringToMapPropertyEditor extends PropertyEditorSupport {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public void setAsText(String text) throws IllegalArgumentException {
|
||||
try {
|
||||
TypeReference<HashMap<String, String>> typeRef =
|
||||
new TypeReference<HashMap<String, String>>() {};
|
||||
Map<String, String> map = objectMapper.readValue(text, typeRef);
|
||||
setValue(map);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Failed to convert java.lang.String to java.util.Map");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,6 @@ multipart.enabled=true
|
||||
logging.level.org.springframework=WARN
|
||||
logging.level.org.hibernate=WARN
|
||||
logging.level.org.eclipse.jetty=WARN
|
||||
#logging.level.org.springframework.security.saml2=TRACE
|
||||
#logging.level.org.springframework.security=DEBUG
|
||||
#logging.level.org.opensaml=DEBUG
|
||||
#logging.level.stirling.software.SPDF.config.security: DEBUG
|
||||
logging.level.com.zaxxer.hikari=WARN
|
||||
|
||||
spring.jpa.open-in-view=false
|
||||
@@ -31,8 +27,6 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
|
||||
|
||||
spring.devtools.restart.enabled=true
|
||||
spring.devtools.livereload.enabled=true
|
||||
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
|
||||
|
||||
spring.thymeleaf.encoding=UTF-8
|
||||
|
||||
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
|
||||
@@ -41,7 +35,7 @@ spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
|
||||
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
|
||||
#spring.thymeleaf.cache=false
|
||||
|
||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driver-class-name=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,11 +81,10 @@ page=صفحة
|
||||
pages=صفحات
|
||||
loading=جارٍ التحميل...
|
||||
addToDoc=إضافة إلى المستند
|
||||
reset=إعداة ضبط
|
||||
|
||||
legal.privacy=سياسة الخصوصية
|
||||
legal.terms=شروط الاستخدام
|
||||
legal.accessibility=إمكانية الوصول
|
||||
legal.accessibility=Accessibility
|
||||
legal.cookie=سياسة ملفات تعريف الارتباط
|
||||
legal.impressum=بيان الهوية
|
||||
|
||||
@@ -119,8 +118,8 @@ pipelineOptions.validateButton=تحقق
|
||||
########################
|
||||
enterpriseEdition.button=ترقية إلى محترف
|
||||
enterpriseEdition.warning=هذه الخاصية متوفرة فقط للمستخدمين المحترفين.
|
||||
enterpriseEdition.yamlAdvert=يدعم Stirling PDF Pro ملفات الإعدادات YAML وميزات SSO أخرى
|
||||
enterpriseEdition.ssoAdvert=هل تبحث عن المزيد من ميزات إدارة المستخدمين؟ اطلع على Stirling PDF Pro
|
||||
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
|
||||
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
|
||||
|
||||
|
||||
#################
|
||||
@@ -142,7 +141,7 @@ navbar.language=اللغات
|
||||
navbar.settings=إعدادات
|
||||
navbar.allTools=أدوات
|
||||
navbar.multiTool=أدوات متعددة
|
||||
navbar.search=البحث
|
||||
navbar.search=Search
|
||||
navbar.sections.organize=تنظيم
|
||||
navbar.sections.convertTo=تحويل الى PDF
|
||||
navbar.sections.convertFrom=تحويل من PDF
|
||||
@@ -247,8 +246,8 @@ database.fileNotFound=لم يتم العثور على الملف
|
||||
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
|
||||
database.failedImportFile=فشل استيراد الملف
|
||||
|
||||
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى
|
||||
session.refreshPage=تحديث الصفحة
|
||||
session.expired=Your session has expired. Please refresh the page and try again.
|
||||
session.refreshPage=Refresh Page
|
||||
|
||||
#############
|
||||
# HOME-PAGE #
|
||||
@@ -512,20 +511,16 @@ home.splitPdfByChapters.title=تجزئة المستندات PDF حسب الفص
|
||||
home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله.
|
||||
splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=إستبدال-عكس اللون
|
||||
replace-color.header=استبدال-عكس لون PDF
|
||||
home.replaceColorPdf.title=إستبدال و عكس الألوان
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=استبدال-إلغاء مirro لون PDF
|
||||
home.replaceColorPdf.title=Replace and Invert Color
|
||||
home.replaceColorPdf.desc=استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف
|
||||
replaceColorPdf.tags=استبدال اللون، عمليات الصفحة، الخلفية، جانب الخادم
|
||||
replace-color.selectText.1=خيارات استبدال أو عكس الألوان
|
||||
replace-color.selectText.2=افتراضي(ألوان التباين العالي الافتراضية)
|
||||
replace-color.selectText.1=خيارات استبدال-إلغاء مirro لون
|
||||
replace-color.selectText.2=Default(Default high contrast colors)
|
||||
replace-color.selectText.3=خصيصة (ألوان شخصية)
|
||||
replace-color.selectText.4=عكس كامل(عكس جميع الألوان)
|
||||
replace-color.selectText.4=Full-Invert(Invert all colors)
|
||||
replace-color.selectText.5=خيارات ألوان التباين العالي
|
||||
replace-color.selectText.6=نص أبيض على خلفية سوداء
|
||||
replace-color.selectText.7=نص أسود على خلفية بيضاء
|
||||
@@ -822,12 +817,7 @@ sign.save=حفظ توقيع
|
||||
sign.personalSigs=توقيعات شخصية
|
||||
sign.sharedSigs=توقيعات مشتركة
|
||||
sign.noSavedSigs=لم يتم العثور على توقيعات محفوظة
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=إصلاح
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=وضع التعرف الضوئي على الحروف
|
||||
ocr.selectText.11=إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل)
|
||||
ocr.selectText.12=نوع العرض (متقدم)
|
||||
ocr.help=يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker
|
||||
ocr.credit=تستخدم هذه الخدمة qpdf و Tesseract للتعرف الضوئي على الحروف.
|
||||
ocr.credit=تستخدم هذه الخدمة OCRmyPDF و Tesseract للتعرف الضوئي على الحروف.
|
||||
ocr.submit=معالجة PDF باستخدام OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=تحويل إلى PDF
|
||||
#compress
|
||||
compress.title=ضغط
|
||||
compress.header=ضغط ملف PDF
|
||||
compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF.
|
||||
compress.credit=تستخدم هذه الخدمة OCRmyPDF لضغط / تحسين PDF.
|
||||
compress.selectText.1=الوضع اليدوي - من 1 إلى 4
|
||||
compress.selectText.2=مستوى التحسين:
|
||||
compress.selectText.3=4 (رهيب للصور النصية)
|
||||
@@ -944,29 +934,17 @@ pdfOrganiser.placeholder=(مثال: 1,3,2 أو 4-8,2,10-12 أو 2n-1)
|
||||
multiTool.title=أداة متعددة PDF
|
||||
multiTool.header=أداة متعددة PDF
|
||||
multiTool.uploadPrompts=اسم الملف
|
||||
multiTool.selectAll=تحديد الكل
|
||||
multiTool.deselectAll=إلغاء تحديد الكل
|
||||
multiTool.selectPages=تحديد الصفحة
|
||||
multiTool.selectedPages=الصفحات المحددة
|
||||
multiTool.page=صفحة
|
||||
multiTool.deleteSelected=حذف المحدد
|
||||
multiTool.downloadAll=تصدير
|
||||
multiTool.downloadSelected=تصدير المحدد
|
||||
|
||||
multiTool.insertPageBreak=إدراج فاصل صفحات
|
||||
multiTool.addFile=إضافة ملف
|
||||
multiTool.rotateLeft=تدوير إلى اليسار
|
||||
multiTool.rotateRight=تدوير إلى اليمين
|
||||
multiTool.split=تقسيم
|
||||
multiTool.moveLeft=تحريك إلى اليسار
|
||||
multiTool.moveRight=تحريك إلى اليمين
|
||||
multiTool.delete=حذف
|
||||
multiTool.dragDropMessage=الصفحات المحددة
|
||||
multiTool.undo=تراجع
|
||||
multiTool.redo=إعادة إجراء
|
||||
multiTool.selectAll=Select All
|
||||
multiTool.deselectAll=Deselect All
|
||||
multiTool.selectPages=Page Select
|
||||
multiTool.selectedPages=Selected Pages
|
||||
multiTool.page=Page
|
||||
multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
#view pdf
|
||||
viewPdf.title=عرض PDF
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=تشفير
|
||||
#watermark
|
||||
watermark.title=إضافة علامة مائية
|
||||
watermark.header=إضافة علامة مائية
|
||||
watermark.customColor=لون نص مخصص
|
||||
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
|
||||
watermark.selectText.2=نص العلامة المائية:
|
||||
watermark.selectText.3=حجم الخط:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=تغيير
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF إلى PDF/A
|
||||
pdfToPDFA.header=PDF إلى PDF/A
|
||||
pdfToPDFA.credit=تستخدم هذه الخدمة qpdf لتحويل PDF/A.
|
||||
pdfToPDFA.credit=تستخدم هذه الخدمة ghostscript لتحويل PDF/A.
|
||||
pdfToPDFA.submit=تحويل
|
||||
pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد
|
||||
pdfToPDFA.outputFormat=تنسيق الإخراج
|
||||
@@ -1261,57 +1238,9 @@ splitByChapters.title=تجزئة المستند حسب الفصول
|
||||
splitByChapters.header=تجزئة المستند حسب الفصول
|
||||
splitByChapters.bookmarkLevel=مستوى العلامات التذكارية
|
||||
splitByChapters.includeMetadata=شامل البيانات المرفقة
|
||||
splitByChapters.allowDuplicates=السماح بالتكرار
|
||||
splitByChapters.desc.1=هذه الأداة تقوم بتقسيم ملف PDF إلى عدة ملفات PDF استناداً إلى بنية فصوله
|
||||
splitByChapters.desc.2=مستوى الإشارة المرجعية: اختر مستوى الإشارات المرجعية التي تريد استخدامها للتقسيم (0 للمستوى الأعلى، 1 للمستوى الثاني، وما إلى ذلك)
|
||||
splitByChapters.allowDuplicates=Allow Duplicates
|
||||
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
|
||||
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
|
||||
splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
|
||||
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
|
||||
splitByChapters.submit=تقطيع ملف PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=انقر هنا
|
||||
fileChooser.or=أو
|
||||
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
|
||||
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -81,7 +81,6 @@ page=Страница
|
||||
pages=Страници
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Политика за поверителност
|
||||
legal.terms=Правила и условия
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Разделете PDF по глави
|
||||
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
|
||||
splitPdfByChapters.tags=разделяне, глави, отметки, организиране
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Замени-инвертиране-на-цвят
|
||||
replace-color.header=Замяна-инвертиране на цвят PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Поправи
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR режим
|
||||
ocr.selectText.11=Премахване на изображения след OCR (Премахва ВСИЧКИ изображения, полезно само ако е част от стъпката на преобразуване)
|
||||
ocr.selectText.12=Тип изобразяване (Разширен)
|
||||
ocr.help=Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker
|
||||
ocr.credit=Тази услуга използва qpdf и Tesseract за OCR.
|
||||
ocr.credit=Тази услуга използва OCRmyPDF и Tesseract за OCR.
|
||||
ocr.submit=Обработка на PDF чрез OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Преобразуване към PDF
|
||||
#compress
|
||||
compress.title=Компресиране
|
||||
compress.header=Компресиране на PDF
|
||||
compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране.
|
||||
compress.credit=Тази услуга използва Ghostscript за PDF компресиране/оптимизиране.
|
||||
compress.selectText.1=Ръчен режим - от 1 до 4
|
||||
compress.selectText.2=Ниво на оптимизация:
|
||||
compress.selectText.3=4 (Ужасно за текстови изображения)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Шифроване
|
||||
#watermark
|
||||
watermark.title=Добавяне на воден знак
|
||||
watermark.header=Добавяне на воден знак
|
||||
watermark.customColor=Персонализиран цвят на текста
|
||||
watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
|
||||
watermark.selectText.2=Текст на воден знак:
|
||||
watermark.selectText.3=Размер на шрифта:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Промени
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF към PDF/A
|
||||
pdfToPDFA.header=PDF към PDF/A
|
||||
pdfToPDFA.credit=Тази услуга използва qpdf за PDF/A преобразуване.
|
||||
pdfToPDFA.credit=Тази услуга използва ghostscript за PDF/A преобразуване.
|
||||
pdfToPDFA.submit=Преобразуване
|
||||
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
|
||||
pdfToPDFA.outputFormat=Изходен формат
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Ниво на отметка: Изберете нивот
|
||||
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
|
||||
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
|
||||
splitByChapters.submit=Разделяне на PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Pàgina
|
||||
pages=Pàgines
|
||||
loading=Carregant...
|
||||
addToDoc=Afegeix al document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacitat
|
||||
legal.terms=Termes i condicions
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Divideix PDF per Capítols
|
||||
home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Reemplaça-Inverteix-Color
|
||||
replace-color.header=Reemplaça-Inverteix Color en PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Desa Signatura
|
||||
sign.personalSigs=Signatures Personals
|
||||
sign.sharedSigs=Signatures Compartides
|
||||
sign.noSavedSigs=No s'han trobat signatures desades
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparar
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Mode OCR
|
||||
ocr.selectText.11=Elimina Imatges després de l'OCR (Elimina TOTES les imatges, útil si forma part d'un procés de conversió)
|
||||
ocr.selectText.12=Tipus de Renderització (Avançat)
|
||||
ocr.help=Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker
|
||||
ocr.credit=Aquest servei fa servir qpdf i Tesseract per a OCR.
|
||||
ocr.credit=Aquest servei fa servir OCRmyPDF i Tesseract per a OCR.
|
||||
ocr.submit=Processa PDF amb OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Converteix a PDF
|
||||
#compress
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Aquest servei utilitza qpdf per a la compressió/optimització de PDF.
|
||||
compress.credit=Aquest servei utilitza Ghostscript per a la compressió/optimització de PDF.
|
||||
compress.selectText.1=Mode manual: de l'1 al 4
|
||||
compress.selectText.2=Nivell d'optimització:
|
||||
compress.selectText.3=4 (terrible per a imatges de text)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Encripta
|
||||
#watermark
|
||||
watermark.title=Afegir Marca d'Aigua
|
||||
watermark.header=Afegir Marca d'Aigua
|
||||
watermark.customColor=Color de Text Personalitzat
|
||||
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
|
||||
watermark.selectText.2=Text de la Marca d'Aigua
|
||||
watermark.selectText.3=Mida de la Font:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Canvia
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF a PDF/A
|
||||
pdfToPDFA.header=PDF a PDF/A
|
||||
pdfToPDFA.credit=Utilitza qpdf per a la conversió a PDF/A
|
||||
pdfToPDFA.credit=Utilitza Ghostscript per a la conversió a PDF/A
|
||||
pdfToPDFA.submit=Converteix
|
||||
pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps
|
||||
pdfToPDFA.outputFormat=Format de sortida
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Nivell de Marcadors: Tria el nivell de marcadors que s'ut
|
||||
splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit.
|
||||
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
|
||||
splitByChapters.submit=Divideix PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Strana
|
||||
pages=Strany
|
||||
loading=Načítání...
|
||||
addToDoc=Přidat do dokumentu
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Politika soukromí
|
||||
legal.terms=Podmínky použití
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Rozdělit PDF podle kapitol
|
||||
home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol.
|
||||
splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Nahradit inverzní barvu PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Uložit podpis
|
||||
sign.personalSigs=Osobní podpisy
|
||||
sign.sharedSigs=Sdílené podpisy
|
||||
sign.noSavedSigs=Nenašly se žádné uložené podpisy
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Opravit
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Režim OCR
|
||||
ocr.selectText.11=Odstranit obrázky po OCR (Odstraní VŠECHNY obrázky, užitečné pouze jako součást kroku konverze)
|
||||
ocr.selectText.12=Typ vykreslení (Pokročilé)
|
||||
ocr.help=Prosím, přečtěte si tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker
|
||||
ocr.credit=Tato služba používá qpdf a Tesseract pro OCR.
|
||||
ocr.credit=Tato služba používá OCRmyPDF a Tesseract pro OCR.
|
||||
ocr.submit=Zpracovat PDF s OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Převést na PDF
|
||||
#compress
|
||||
compress.title=Komprese
|
||||
compress.header=Komprimovat PDF
|
||||
compress.credit=Tato služba používá qpdf pro kompresi/optimalizaci PDF.
|
||||
compress.credit=Tato služba používá Ghostscript pro kompresi/optimalizaci PDF.
|
||||
compress.selectText.1=Ruční režim - Od 1 do 4
|
||||
compress.selectText.2=Úroveň optimalizace:
|
||||
compress.selectText.3=4 (Hrozné pro textové obrázky)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Šifrovat
|
||||
#watermark
|
||||
watermark.title=Přidat vodoznak
|
||||
watermark.header=Přidat vodoznak
|
||||
watermark.customColor=Vlastní barva textu
|
||||
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
|
||||
watermark.selectText.2=Text vodoznaku:
|
||||
watermark.selectText.3=Velikost písma:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Změnit
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF na PDF/A
|
||||
pdfToPDFA.header=PDF na PDF/A
|
||||
pdfToPDFA.credit=Tato služba používá qpdf pro konverzi do formátu PDF/A
|
||||
pdfToPDFA.credit=Tato služba používá ghostscript pro konverzi do formátu PDF/A
|
||||
pdfToPDFA.submit=Převést
|
||||
pdfToPDFA.tip=V současné době nepracuje pro více vstupů najednou
|
||||
pdfToPDFA.outputFormat=Výstupní formát
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Úroveň záhlaví: Zvolte úroveň záhlaví pro použit
|
||||
splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru.
|
||||
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
|
||||
splitByChapters.submit=Podělit se PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Sidenummer
|
||||
pages=Sideantal
|
||||
loading=Laster...
|
||||
addToDoc=Tilføj til Dokument
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Vilkår og betingelser
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Partitioner PDF efter kapitler
|
||||
home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur.
|
||||
splitPdfByChapters.tags=partitionering,kapitler,merker,organisering
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Erstat-omgivende Farve PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Gem Signatur
|
||||
sign.personalSigs=Personlige Signaturer
|
||||
sign.sharedSigs=Delte Signaturer
|
||||
sign.noSavedSigs=Ingen Gemte Signaturer Fundet
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparér
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR-tilstand
|
||||
ocr.selectText.11=Fjern billeder efter OCR (Fjerner ALLE billeder, kun nyttigt hvis det er en del af konverteringstrinnet)
|
||||
ocr.selectText.12=Renderingstype (Avanceret)
|
||||
ocr.help=Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker
|
||||
ocr.credit=Denne tjeneste bruger qpdf og Tesseract til OCR.
|
||||
ocr.credit=Denne tjeneste bruger OCRmyPDF og Tesseract til OCR.
|
||||
ocr.submit=Behandl PDF med OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Konvertér til PDF
|
||||
#compress
|
||||
compress.title=Komprimer
|
||||
compress.header=Komprimer PDF
|
||||
compress.credit=Denne tjeneste bruger qpdf til PDF Komprimering/Optimering.
|
||||
compress.credit=Denne tjeneste bruger Ghostscript til PDF Komprimering/Optimering.
|
||||
compress.selectText.1=Manuel Tilstand - Fra 1 til 4
|
||||
compress.selectText.2=Optimeringsniveau:
|
||||
compress.selectText.3=4 (Forfærdelig for tekstbilleder)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Kryptér
|
||||
#watermark
|
||||
watermark.title=Tilføj Vandmærke
|
||||
watermark.header=Tilføj Vandmærke
|
||||
watermark.customColor=Brugerdefineret Tekstfarve
|
||||
watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
|
||||
watermark.selectText.2=Vandmærketekst:
|
||||
watermark.selectText.3=Skriftstørrelse:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Ændre
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF Til PDF/A
|
||||
pdfToPDFA.header=PDF Til PDF/A
|
||||
pdfToPDFA.credit=Denne tjeneste bruger qpdf til PDF/A-konvertering
|
||||
pdfToPDFA.credit=Denne tjeneste bruger ghostscript til PDF/A-konvertering
|
||||
pdfToPDFA.submit=Konvertér
|
||||
pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang
|
||||
pdfToPDFA.outputFormat=Outputformat
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bogmærke niveau: Vælg nivået af bogmærker, der skal b
|
||||
splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF.
|
||||
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
|
||||
splitByChapters.submit=Splitter PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Seite
|
||||
pages=Seiten
|
||||
loading=Laden...
|
||||
addToDoc=In Dokument hinzufügen
|
||||
reset=Zurücksetzen
|
||||
|
||||
legal.privacy=Datenschutz
|
||||
legal.terms=AGB
|
||||
@@ -146,8 +145,8 @@ navbar.search=Suche
|
||||
navbar.sections.organize=Organisieren
|
||||
navbar.sections.convertTo=In PDF konvertieren
|
||||
navbar.sections.convertFrom=Konvertieren von PDF
|
||||
navbar.sections.security=Signieren und Sicherheit
|
||||
navbar.sections.advance=Erweiterte Funktionen
|
||||
navbar.sections.security=Zeichen und Sicherheit
|
||||
navbar.sections.advance=Fortschrittlich
|
||||
navbar.sections.edit=Anzeigen und Bearbeiten
|
||||
navbar.sections.popular=Beliebt
|
||||
|
||||
@@ -248,7 +247,7 @@ database.fileNullOrEmpty=Datei darf nicht null oder leer sein
|
||||
database.failedImportFile=Dateiimport fehlgeschlagen
|
||||
|
||||
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
||||
session.refreshPage=Seite aktualisieren
|
||||
session.refreshPage=Refresh Page
|
||||
|
||||
#############
|
||||
# HOME-PAGE #
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=PDF-Datei nach Kapiteln aufteilen
|
||||
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
|
||||
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
|
||||
|
||||
home.validateSignature.title=PDF-Signatur überprüfen
|
||||
home.validateSignature.desc=Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen
|
||||
validateSignature.tags=signature,verify,validate,pdf,digitale signatur,signatur validieren,überprüfen,Zertifikat,cert
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Farbe Ersetzen-Invertieren
|
||||
replace-color.header=Farb-PDF Ersetzen-Invertieren
|
||||
@@ -822,12 +817,7 @@ sign.save=Signature speichern
|
||||
sign.personalSigs=Persönliche Signaturen
|
||||
sign.sharedSigs=Geteilte Signaturen
|
||||
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
|
||||
sign.addToAll=Zu allen Seiten hinzufügen
|
||||
sign.delete=Löschen
|
||||
sign.first=Erste Seite
|
||||
sign.last=Letzte Seite
|
||||
sign.next=Nächste Seite
|
||||
sign.previous=Vorherige Seite
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparieren
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR-Modus
|
||||
ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts)
|
||||
ocr.selectText.12=Rendertyp (Erweitert)
|
||||
ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können
|
||||
ocr.credit=Dieser Dienst verwendet qpdf und Tesseract für OCR.
|
||||
ocr.credit=Dieser Dienst verwendet OCRmyPDF und Tesseract für OCR.
|
||||
ocr.submit=PDF mit OCR verarbeiten
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=In PDF konvertieren
|
||||
#compress
|
||||
compress.title=Komprimieren
|
||||
compress.header=PDF komprimieren
|
||||
compress.credit=Dieser Dienst verwendet qpdf für die PDF-Komprimierung/-Optimierung.
|
||||
compress.credit=Dieser Dienst verwendet Ghostscript für die PDF-Komprimierung/-Optimierung.
|
||||
compress.selectText.1=Manueller Modus – Von 1 bis 4
|
||||
compress.selectText.2=Optimierungsstufe:
|
||||
compress.selectText.3=4 (Schrecklich für Textbilder)
|
||||
@@ -953,20 +943,8 @@ multiTool.deleteSelected=Auswahl löschen
|
||||
multiTool.downloadAll=Downloaden
|
||||
multiTool.downloadSelected=Auswahl downloaden
|
||||
|
||||
multiTool.insertPageBreak=Seitenumbruch einfügen
|
||||
multiTool.addFile=Datei hinzufügen
|
||||
multiTool.rotateLeft=Nach links drehen
|
||||
multiTool.rotateRight=Nach rechts drehen
|
||||
multiTool.split=Teilen
|
||||
multiTool.moveLeft=Nach links verschieben
|
||||
multiTool.moveRight=Nach rechts verschieben
|
||||
multiTool.delete=Löschen
|
||||
multiTool.dragDropMessage=Ausgewählte Seite(n)
|
||||
multiTool.undo=Rückgängig machen
|
||||
multiTool.redo=Wiederherstellen
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen!
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
#view pdf
|
||||
viewPdf.title=PDF anzeigen
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Verschlüsseln
|
||||
#watermark
|
||||
watermark.title=Wasserzeichen hinzufügen
|
||||
watermark.header=Wasserzeichen hinzufügen
|
||||
watermark.customColor=Benutzerdefinierte Textfarbe
|
||||
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
|
||||
watermark.selectText.2=Wasserzeichen Text:
|
||||
watermark.selectText.3=Schriftgröße:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Ändern
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF zu PDF/A
|
||||
pdfToPDFA.header=PDF zu PDF/A
|
||||
pdfToPDFA.credit=Dieser Dienst verwendet qpdf für die PDF/A-Konvertierung
|
||||
pdfToPDFA.credit=Dieser Dienst verwendet ghostscript für die PDF/A-Konvertierung
|
||||
pdfToPDFA.submit=Konvertieren
|
||||
pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten.
|
||||
pdfToPDFA.outputFormat=Ausgabeformat
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Lesezeichenebene: Wählen Sie die Ebene der Lesezeichen,
|
||||
splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen.
|
||||
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
|
||||
splitByChapters.submit=PDF teilen
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Klicken
|
||||
fileChooser.or=oder
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
|
||||
|
||||
#release notes
|
||||
releases.footer=Veröffentlichungen
|
||||
releases.title=Versionshinweise
|
||||
releases.header=Versionshinweise
|
||||
releases.current.version=Aktuelle Version
|
||||
releases.note=Versionshinweise sind nur auf Englisch verfügbar
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=PDF-Signaturen überprüfen
|
||||
validateSignature.header=Digitale Signaturen überprüfen
|
||||
validateSignature.selectPDF=Signierte PDF-Datei auswählen
|
||||
validateSignature.submit=Signaturen überprüfen
|
||||
validateSignature.results=Gültigkeitsprüfungsergebnisse
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Unterzeichner
|
||||
validateSignature.date=Datum
|
||||
validateSignature.reason=Grund
|
||||
validateSignature.location=Ort
|
||||
validateSignature.noSignatures=Keine digitalen Signaturen in diesem Dokument gefunden
|
||||
validateSignature.status.valid=Gültig
|
||||
validateSignature.status.invalid=Ungültig
|
||||
validateSignature.chain.invalid=Zertifikatskettenprüfung fehlgeschlagen - kann die Identität des Unterzeichners nicht verifizieren
|
||||
validateSignature.trust.invalid=Zertifikat nicht im Truststore - Quelle kann nicht verifiziert werden
|
||||
validateSignature.cert.expired=Zertifikat ist abgelaufen
|
||||
validateSignature.cert.revoked=Zertifikat wurde widerrufen
|
||||
validateSignature.signature.info=Signaturinformationen
|
||||
validateSignature.signature=Signatur
|
||||
validateSignature.signature.mathValid=Signatur ist mathematisch gültig ABER:
|
||||
validateSignature.selectCustomCert=Benutzerdefinierte Zertifikatsdatei X.509 (Optional)
|
||||
validateSignature.cert.info=Zertifikat Details
|
||||
validateSignature.cert.issuer=Aussteller
|
||||
validateSignature.cert.subject=Betreff
|
||||
validateSignature.cert.serialNumber=Seriennummer
|
||||
validateSignature.cert.validFrom=Gültig von
|
||||
validateSignature.cert.validUntil=Gültig bis
|
||||
validateSignature.cert.algorithm=Algorithmus
|
||||
validateSignature.cert.keySize=Schlüsselgröße
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Schlüsselverwendung
|
||||
validateSignature.cert.selfSigned=Selbstsigniert
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Σελίδα
|
||||
pages=Σελίδες
|
||||
loading=Φόρτωση...
|
||||
addToDoc=Πρόσθεση στο Εκπομπώματο
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Πολιτική Προνομίους
|
||||
legal.terms=Φράσεις Υποχρεωτικότητας
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Διχοτομία PDF ανά Περιγραφές
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=διχοτομία,περιγραφές,κεφάλαια,συνορία
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Αντικατάσταση-Αντίστροφη Παθωμένη Πίντσουρ
|
||||
@@ -822,12 +817,7 @@ sign.save=Αποθήκευση Αλιάσης
|
||||
sign.personalSigs=Προσωπικές Αλιάσεις
|
||||
sign.sharedSigs=Μεταδότες Αλιάσεις
|
||||
sign.noSavedSigs=Δεν βρέθηκαν αποθηκευμένες αλιάσεις
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Επιδιόρθωση
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Λειτουργία OCR
|
||||
ocr.selectText.11=Κατάργηση εικόνων μετά το OCR (Καταργεί ΟΛΕΣ τις εικόνες, είναι χρήσιμο μόνο αν αποτελεί μέρος του βήματος μετατροπής)
|
||||
ocr.selectText.12=Τύπος απόδοσης (Για προχωρημένους)
|
||||
ocr.help=Διαβάστε αυτήν την τεκμηρίωση σχετικά με τον τρόπο χρήσης αυτής για άλλες γλώσσες ή/και μη χρήσης σε docker
|
||||
ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf και Tesseract για OCR.
|
||||
ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί OCRmyPDF και Tesseract για OCR.
|
||||
ocr.submit=Επεξεργασία PDF με OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Μετατροπή σε PDF
|
||||
#compress
|
||||
compress.title=Συμπίεση
|
||||
compress.header=Συμπίεση PDF
|
||||
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF Συμπίεση/Βελτιστοποίηση.
|
||||
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί Ghostscript για PDF Συμπίεση/Βελτιστοποίηση.
|
||||
compress.selectText.1=Χειροκίνητη Λειτουργία - Από 1 έως 4
|
||||
compress.selectText.2=Επίπεδο Βελτιστοποίησης:
|
||||
compress.selectText.3=4 (Πολύ κακό για εικόνες κειμένου)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,13 +1034,12 @@ addPassword.submit=Κρυπτογράφηση
|
||||
#watermark
|
||||
watermark.title=Προσθήκη Υδατογραφήματος
|
||||
watermark.header=Προσθήκη Υδατογραφήματος
|
||||
watermark.customColor=Προσαρμοσμένο χρώμα κειμένου
|
||||
watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος:
|
||||
watermark.selectText.2=Κείμενο Υδατογραφήματος:
|
||||
watermark.selectText.3=Μέγεθος Κειμένου:
|
||||
watermark.selectText.4=Περιστροφή (0-360):
|
||||
watermark.selectText.5=Width Spacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
|
||||
watermark.selectText.6=Height Spacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
|
||||
watermark.selectText.5=widthSpacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
|
||||
watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
|
||||
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
|
||||
watermark.selectText.8=Τύπος Υδατογραφήματος:
|
||||
watermark.selectText.9=Εικόνα Υδατογραφήματος:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Αλλαγή
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF σε PDF/A
|
||||
pdfToPDFA.header=PDF σε PDF/A
|
||||
pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF/A μετατροπή
|
||||
pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί ghostscript για PDF/A μετατροπή
|
||||
pdfToPDFA.submit=Μετατροπή
|
||||
pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα
|
||||
pdfToPDFA.outputFormat=Εξόδος αναμορφώσεων
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Επίπεδο Σήμανσης Σκέψης: Επιλέ
|
||||
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
|
||||
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
|
||||
splitByChapters.submit=Διαλύστε το PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -73,7 +73,7 @@ joinDiscord=Join our Discord server
|
||||
seeDockerHub=See Docker Hub
|
||||
visitGithub=Visit Github Repository
|
||||
donate=Donate
|
||||
color=Colour
|
||||
color=Color
|
||||
sponsor=Sponsor
|
||||
info=Info
|
||||
pro=Pro
|
||||
@@ -419,9 +419,9 @@ home.auto-rename.title=Auto Rename PDF File
|
||||
home.auto-rename.desc=Auto renames a PDF file based on its detected header
|
||||
auto-rename.tags=auto-detect,header-based,organize,relabel
|
||||
|
||||
home.adjust-contrast.title=Adjust Colours/Contrast
|
||||
home.adjust-contrast.title=Adjust Colors/Contrast
|
||||
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
|
||||
adjust-contrast.tags=color-correction,tune,modify,enhance,colour-correction
|
||||
adjust-contrast.tags=color-correction,tune,modify,enhance
|
||||
|
||||
home.crop.title=Crop PDF
|
||||
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
|
||||
@@ -488,11 +488,11 @@ overlay-pdfs.tags=Overlay
|
||||
|
||||
home.split-by-sections.title=Split PDF by Sections
|
||||
home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections
|
||||
split-by-sections.tags=Section Split, Divide, Customize,Customise
|
||||
split-by-sections.tags=Section Split, Divide, Customize
|
||||
|
||||
home.AddStampRequest.title=Add Stamp to PDF
|
||||
home.AddStampRequest.desc=Add text or add image stamps at set locations
|
||||
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize,Customise
|
||||
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize
|
||||
|
||||
|
||||
home.PDFToBook.title=PDF to Book
|
||||
@@ -512,27 +512,23 @@ home.splitPdfByChapters.title=Split PDF by Chapters
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Advanced Colour options
|
||||
replace-color.header=Replace-Invert Colour PDF
|
||||
replace-color.header=Replace-Invert Color PDF
|
||||
home.replaceColorPdf.title=Advanced Colour options
|
||||
home.replaceColorPdf.desc=Replace colour for text and background in PDF and invert full colour of pdf to reduce file size
|
||||
replaceColorPdf.tags=Replace Colour,Page operations,Back end,server side
|
||||
replace-color.selectText.1=Replace or Invert colour Options
|
||||
replace-color.selectText.2=Default(Default high contrast colours)
|
||||
replace-color.selectText.3=Custom(Customised colours)
|
||||
replace-color.selectText.4=Full-Invert(Invert all colours)
|
||||
replace-color.selectText.5=High contrast colour options
|
||||
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
|
||||
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
|
||||
replace-color.selectText.1=Replace or Invert color Options
|
||||
replace-color.selectText.2=Default(Default high contrast colors)
|
||||
replace-color.selectText.3=Custom(Customized colors)
|
||||
replace-color.selectText.4=Full-Invert(Invert all colors)
|
||||
replace-color.selectText.5=High contrast color options
|
||||
replace-color.selectText.6=white text on black background
|
||||
replace-color.selectText.7=Black text on white background
|
||||
replace-color.selectText.8=Yellow text on black background
|
||||
replace-color.selectText.9=Green text on black background
|
||||
replace-color.selectText.10=Choose text Colour
|
||||
replace-color.selectText.11=Choose background Colour
|
||||
replace-color.selectText.10=Choose text Color
|
||||
replace-color.selectText.11=Choose background Color
|
||||
replace-color.submit=Replace
|
||||
|
||||
|
||||
@@ -655,7 +651,7 @@ AddStampRequest.position=Position
|
||||
AddStampRequest.overrideX=Override X Coordinate
|
||||
AddStampRequest.overrideY=Override Y Coordinate
|
||||
AddStampRequest.customMargin=Custom Margin
|
||||
AddStampRequest.customColor=Custom Text Colour
|
||||
AddStampRequest.customColor=Custom Text Color
|
||||
AddStampRequest.submit=Submit
|
||||
|
||||
|
||||
@@ -787,8 +783,8 @@ removeAnnotations.submit=Remove
|
||||
#compare
|
||||
compare.title=Compare
|
||||
compare.header=Compare PDFs
|
||||
compare.highlightColor.1=Highlight Colour 1:
|
||||
compare.highlightColor.2=Highlight Colour 2:
|
||||
compare.highlightColor.1=Highlight Color 1:
|
||||
compare.highlightColor.2=Highlight Color 2:
|
||||
compare.document.1=Document 1
|
||||
compare.document.2=Document 2
|
||||
compare.submit=Compare
|
||||
@@ -822,12 +818,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Repair
|
||||
@@ -846,7 +837,7 @@ flatten.submit=Flatten
|
||||
ScannerImageSplit.selectText.1=Angle Threshold:
|
||||
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
|
||||
ScannerImageSplit.selectText.3=Tolerance:
|
||||
ScannerImageSplit.selectText.4=Determines the range of colour variation around the estimated background colour (default: 30).
|
||||
ScannerImageSplit.selectText.4=Determines the range of color variation around the estimated background color (default: 30).
|
||||
ScannerImageSplit.selectText.5=Minimum Area:
|
||||
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000).
|
||||
ScannerImageSplit.selectText.7=Minimum Contour Area:
|
||||
@@ -872,7 +863,7 @@ ocr.selectText.10=OCR Mode
|
||||
ocr.selectText.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
|
||||
ocr.selectText.12=Render Type (Advanced)
|
||||
ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker
|
||||
ocr.credit=This service uses qpdf and Tesseract for OCR.
|
||||
ocr.credit=This service uses OCRmyPDF and Tesseract for OCR.
|
||||
ocr.submit=Process PDF with OCR
|
||||
|
||||
|
||||
@@ -896,9 +887,9 @@ fileToPDF.submit=Convert to PDF
|
||||
#compress
|
||||
compress.title=Compress
|
||||
compress.header=Compress PDF
|
||||
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
|
||||
compress.credit=This service uses Ghostscript for PDF Compress/Optimisation.
|
||||
compress.selectText.1=Manual Mode - From 1 to 4
|
||||
compress.selectText.2=Optimisation level:
|
||||
compress.selectText.2=Optimization level:
|
||||
compress.selectText.3=4 (Terrible for text images)
|
||||
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size
|
||||
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
|
||||
@@ -953,18 +944,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,13 +1035,12 @@ addPassword.submit=Encrypt
|
||||
#watermark
|
||||
watermark.title=Add Watermark
|
||||
watermark.header=Add Watermark
|
||||
watermark.customColor=Custom Text Colour
|
||||
watermark.selectText.1=Select PDF to add watermark to:
|
||||
watermark.selectText.2=Watermark Text:
|
||||
watermark.selectText.3=Font Size:
|
||||
watermark.selectText.4=Rotation (0-360):
|
||||
watermark.selectText.5=Width Spacer (Space between each watermark horizontally):
|
||||
watermark.selectText.6=Height Spacer (Space between each watermark vertically):
|
||||
watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
|
||||
watermark.selectText.6=heightSpacer (Space between each watermark vertically):
|
||||
watermark.selectText.7=Opacity (0% - 100%):
|
||||
watermark.selectText.8=Watermark Type:
|
||||
watermark.selectText.9=Watermark Image:
|
||||
@@ -1119,7 +1097,7 @@ changeMetadata.submit=Change
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF To PDF/A
|
||||
pdfToPDFA.header=PDF To PDF/A
|
||||
pdfToPDFA.credit=This service uses qpdf for PDF/A conversion
|
||||
pdfToPDFA.credit=This service uses ghostscript for PDF/A conversion
|
||||
pdfToPDFA.submit=Convert
|
||||
pdfToPDFA.tip=Currently does not work for multiple inputs at once
|
||||
pdfToPDFA.outputFormat=Output format
|
||||
@@ -1267,51 +1245,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||
splitByChapters.submit=Split PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Replace-Invert Color PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Repair
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR Mode
|
||||
ocr.selectText.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
|
||||
ocr.selectText.12=Render Type (Advanced)
|
||||
ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker
|
||||
ocr.credit=This service uses qpdf and Tesseract for OCR.
|
||||
ocr.credit=This service uses OCRmyPDF and Tesseract for OCR.
|
||||
ocr.submit=Process PDF with OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Convert to PDF
|
||||
#compress
|
||||
compress.title=Compress
|
||||
compress.header=Compress PDF
|
||||
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
|
||||
compress.credit=This service uses Ghostscript for PDF Compress/Optimisation.
|
||||
compress.selectText.1=Manual Mode - From 1 to 4
|
||||
compress.selectText.2=Optimization level:
|
||||
compress.selectText.3=4 (Terrible for text images)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,13 +1034,12 @@ addPassword.submit=Encrypt
|
||||
#watermark
|
||||
watermark.title=Add Watermark
|
||||
watermark.header=Add Watermark
|
||||
watermark.customColor=Custom Text Color
|
||||
watermark.selectText.1=Select PDF to add watermark to:
|
||||
watermark.selectText.2=Watermark Text:
|
||||
watermark.selectText.3=Font Size:
|
||||
watermark.selectText.4=Rotation (0-360):
|
||||
watermark.selectText.5=Width Spacer (Space between each watermark horizontally):
|
||||
watermark.selectText.6=Height Spacer (Space between each watermark vertically):
|
||||
watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
|
||||
watermark.selectText.6=heightSpacer (Space between each watermark vertically):
|
||||
watermark.selectText.7=Opacity (0% - 100%):
|
||||
watermark.selectText.8=Watermark Type:
|
||||
watermark.selectText.9=Watermark Image:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Change
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF To PDF/A
|
||||
pdfToPDFA.header=PDF To PDF/A
|
||||
pdfToPDFA.credit=This service uses qpdf for PDF/A conversion
|
||||
pdfToPDFA.credit=This service uses ghostscript for PDF/A conversion
|
||||
pdfToPDFA.submit=Convert
|
||||
pdfToPDFA.tip=Currently does not work for multiple inputs at once
|
||||
pdfToPDFA.outputFormat=Output format
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||
splitByChapters.submit=Split PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Página
|
||||
pages=Páginas
|
||||
loading=Cargando...
|
||||
addToDoc=Agregar al Documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Política de Privacidad
|
||||
legal.terms=Términos y Condiciones
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Dividir PDF por capítulos
|
||||
home.splitPdfByChapters.desc=Divida un PDF en varios archivos según su estructura de capítulos.
|
||||
splitPdfByChapters.tags=dividir,capítulos,marcadores,organizar
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Reemplazar-Invertir-Color
|
||||
replace-color.header=Reemplazar-Invertir Color en PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Guardar Firma
|
||||
sign.personalSigs=Firmas Personales
|
||||
sign.sharedSigs=Firmas compartidas
|
||||
sign.noSavedSigs=No se encontraron firmas guardadas
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Reparar
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Modo OCR
|
||||
ocr.selectText.11=Eliminar imágenes después de OCR (Elimina TODAS las imágenes, solo es útil si es parte del paso de conversión)
|
||||
ocr.selectText.12=Tipo de procesamiento (avanzado)
|
||||
ocr.help=Lea esta documentación sobre cómo usar esto para otros idiomas y/o no usarlo en Docker
|
||||
ocr.credit=Este servicio utiliza qpdf y Tesseract para OCR
|
||||
ocr.credit=Este servicio utiliza OCRmyPDF y Tesseract para OCR
|
||||
ocr.submit=Procesar PDF con OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Convertir a PDF
|
||||
#compress
|
||||
compress.title=Comprimir
|
||||
compress.header=Comprimir PDF
|
||||
compress.credit=Este servicio utiliza qpdf para compresión/optimización de PDF
|
||||
compress.credit=Este servicio utiliza Ghostscript para compresión/optimización de PDF
|
||||
compress.selectText.1=Modo manual - De 1 a 4
|
||||
compress.selectText.2=Nivel de optimización:
|
||||
compress.selectText.3=4 (Terrible para imágenes de texto)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Encriptar
|
||||
#watermark
|
||||
watermark.title=Añadir marca de agua
|
||||
watermark.header=Añadir marca de agua
|
||||
watermark.customColor=Personalizar color de texto
|
||||
watermark.selectText.1=Seleccionar PDF para añadir marca de agua:
|
||||
watermark.selectText.2=Texto de la marca de agua:
|
||||
watermark.selectText.3=Tamaño de la Fuente:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Cambiar
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF a PDF/A
|
||||
pdfToPDFA.header=PDF a PDF/A
|
||||
pdfToPDFA.credit=Este servicio usa qpdf para la conversión a PDF/A
|
||||
pdfToPDFA.credit=Este servicio usa ghostscript para la conversión a PDF/A
|
||||
pdfToPDFA.submit=Convertir
|
||||
pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez
|
||||
pdfToPDFA.outputFormat=Formato de salida
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Nivel de Marcador: Elige el nivel de marcadores para divi
|
||||
splitByChapters.desc.3=Incluir Metadatos: Si está seleccionado, los metadatos del PDF original se incluirán en cada PDF dividido.
|
||||
splitByChapters.desc.4=Permitir Duplicados: Si está seleccionado, permite que múltiples marcadores en la misma página creen archivos PDF separados.
|
||||
splitByChapters.submit=Dividir PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Replace-Invert Color PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Konpondu
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR modua
|
||||
ocr.selectText.11=Irudiak ezabatu OCR-ren ondoren (Irudi GUZTIAK ezabatzen ditu, bakarrik da erabilgarri bihurketa urratsaren parte baldin bada)
|
||||
ocr.selectText.12=Prozesaketa-mota (aurreratua)
|
||||
ocr.help=Irakurri honen erabilerari buruzko dokumentazioa beste hizkuntza batzuetarako eta/edo ez erabili Docker-en
|
||||
ocr.credit=Zerbitzu honek qpdf eta OCR-rako Tesseract erabiltzen ditu
|
||||
ocr.credit=Zerbitzu honek OCRmyPDF eta OCR-rako Tesseract erabiltzen ditu
|
||||
ocr.submit=PDF prozesatu OCR-rekin
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=PDF bihurtu
|
||||
#compress
|
||||
compress.title=Konprimatu
|
||||
compress.header=PDFa konprimatu
|
||||
compress.credit=Zerbitzu honek qpdf erabiltzen du PDFak komprimatzeko/optimizatzeko
|
||||
compress.credit=Zerbitzu honek Ghostscript erabiltzen du PDFak komprimatzeko/optimizatzeko
|
||||
compress.selectText.1=Eskuz 1etik 4ra
|
||||
compress.selectText.2=Optimizazio maila:
|
||||
compress.selectText.3=4 (Izugarria testu-irudietarako)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Enkriptatu
|
||||
#watermark
|
||||
watermark.title=Gehitu ur-marka
|
||||
watermark.header=Gehitu ur-marka
|
||||
watermark.customColor=Custom Text Color
|
||||
watermark.selectText.1=Hautatu PDFa ur-marka gehitzeko:
|
||||
watermark.selectText.2=Ur-markaren testua:
|
||||
watermark.selectText.3=Letra-tipoaren tamaina:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Aldatu
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDFa PDF/A bihurtu
|
||||
pdfToPDFA.header=PDFa PDF/A bihurtu
|
||||
pdfToPDFA.credit=Zerbitzu honek qpdf erabiltzen du PDFak PDF/A bihurtzeko
|
||||
pdfToPDFA.credit=Zerbitzu honek ghostscript erabiltzen du PDFak PDF/A bihurtzeko
|
||||
pdfToPDFA.submit=Bihurtu
|
||||
pdfToPDFA.tip=Currently does not work for multiple inputs at once
|
||||
pdfToPDFA.outputFormat=Output format
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||
splitByChapters.submit=Split PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -56,8 +56,8 @@ userNotFoundMessage=Utilisateur non trouvé.
|
||||
incorrectPasswordMessage=Le mot de passe actuel est incorrect.
|
||||
usernameExistsMessage=Le nouveau nom d'utilisateur existe déjà.
|
||||
invalidUsernameMessage=Nom d'utilisateur invalide, le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
|
||||
invalidPasswordMessage=Le mot de passe ne peut pas être vide et ne doit pas contenir d'espaces au début ou à la fin.
|
||||
confirmPasswordErrorMessage=Le nouveau mot de passe et sa confirmation doivent être identiques.
|
||||
invalidPasswordMessage=Le mot de passe ne peut pas être vide et ne doit pas contenir d'espaces au début ou en fin.
|
||||
confirmPasswordErrorMessage=Nouveau Mot de passe et Confirmer le Nouveau Mot de passe doivent correspondre.
|
||||
deleteCurrentUserMessage=Impossible de supprimer l'utilisateur actuellement connecté.
|
||||
deleteUsernameExistsMessage=Le nom d'utilisateur n'existe pas et ne peut pas être supprimé.
|
||||
downgradeCurrentUserMessage=Impossible de rétrograder le rôle de l'utilisateur actuel.
|
||||
@@ -81,7 +81,6 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Chargement...
|
||||
addToDoc=Ajouter au Document
|
||||
reset=Réinitialiser
|
||||
|
||||
legal.privacy=Politique de Confidentialité
|
||||
legal.terms=Conditions Générales
|
||||
@@ -142,7 +141,7 @@ navbar.language=Langues
|
||||
navbar.settings=Paramètres
|
||||
navbar.allTools=Outils
|
||||
navbar.multiTool=Outils Multiples
|
||||
navbar.search=Rechercher
|
||||
navbar.search=Search
|
||||
navbar.sections.organize=Organisation
|
||||
navbar.sections.convertTo=Convertir en PDF
|
||||
navbar.sections.convertFrom=Convertir depuis PDF
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Séparer un PDF par chapitres
|
||||
home.splitPdfByChapters.desc=Séparez un PDF en fichiers multiples en fonction de sa structure par chapitres.
|
||||
splitPdfByChapters.tags=séparer,chapitres,split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Remplacer-Inverser-Couleur
|
||||
replace-color.header=Remplacer-Inverser Couleur PDF
|
||||
@@ -817,17 +812,12 @@ sign.draw=Dessiner une signature
|
||||
sign.text=Saisir de texte
|
||||
sign.clear=Effacer
|
||||
sign.add=Ajouter
|
||||
sign.saved=Sceaux enregistrées
|
||||
sign.saved=Saved Signatures
|
||||
sign.save=Enregistrer le sceau
|
||||
sign.personalSigs=Sceaux personnels
|
||||
sign.sharedSigs=Sceaux partagés
|
||||
sign.noSavedSigs=Aucun sceau enregistré trouvé
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Réparer
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Mode OCR
|
||||
ocr.selectText.11=Supprimer les images après l'OCR (Supprime TOUTES les images, utile uniquement si elles font partie de l'étape de conversion)
|
||||
ocr.selectText.12=Type de rendu (avancé)
|
||||
ocr.help=Veuillez lire cette documentation pour savoir comment utiliser l'OCR pour d'autres langues ou une utilisation hors Docker :
|
||||
ocr.credit=Ce service utilise qpdf et Tesseract pour l'OCR.
|
||||
ocr.credit=Ce service utilise OCRmyPDF et Tesseract pour l'OCR.
|
||||
ocr.submit=Traiter
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Convertir
|
||||
#compress
|
||||
compress.title=Compresser un PDF
|
||||
compress.header=Compresser un PDF (lorsque c'est possible!)
|
||||
compress.credit=Ce service utilise qpdf pour la compression et l'optimisation des PDF.
|
||||
compress.credit=Ce service utilise Ghostscript pour la compression et l'optimisation des PDF.
|
||||
compress.selectText.1=Mode manuel – de 1 à 4
|
||||
compress.selectText.2=Niveau d'optimisation
|
||||
compress.selectText.3=4 (terrible pour les images textuelles)
|
||||
@@ -944,29 +934,17 @@ pdfOrganiser.placeholder=(par exemple 1,3,2 ou 4-8,2,10-12 ou 2n-1)
|
||||
multiTool.title=Outil multifonction PDF
|
||||
multiTool.header=Outil multifonction PDF
|
||||
multiTool.uploadPrompts=Nom du fichier
|
||||
multiTool.selectAll=Tout sélectionner
|
||||
multiTool.deselectAll=Tout déselectionner
|
||||
multiTool.selectPages=Sélection des pages
|
||||
multiTool.selectedPages=Pages sélectionnées
|
||||
multiTool.selectAll=Select All
|
||||
multiTool.deselectAll=Deselect All
|
||||
multiTool.selectPages=Page Select
|
||||
multiTool.selectedPages=Selected Pages
|
||||
multiTool.page=Page
|
||||
multiTool.deleteSelected=Supprimer la sélection
|
||||
multiTool.downloadAll=Exporter
|
||||
multiTool.downloadSelected=Exporter la sélection
|
||||
|
||||
multiTool.insertPageBreak=Insérer un saut de page
|
||||
multiTool.addFile=Ajouter un fichier
|
||||
multiTool.rotateLeft=Rotation vers la gauche
|
||||
multiTool.rotateRight=Rotation vers la droite
|
||||
multiTool.split=Diviser
|
||||
multiTool.moveLeft=Déplacer vers la gauche
|
||||
multiTool.moveRight=Déplacer vers la droite
|
||||
multiTool.delete=Supprimer
|
||||
multiTool.dragDropMessage=Page(s) sélectionnées
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=Cette fonctionnalité est aussi disponible dans la <a href="{0}">page de l'outil multifonction</a>. Allez-y pour une interface page par page améliorée et des fonctionnalités additionnelles !
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
#view pdf
|
||||
viewPdf.title=Visualiser un PDF
|
||||
@@ -1056,13 +1034,12 @@ addPassword.submit=Chiffrer
|
||||
#watermark
|
||||
watermark.title=Ajouter un filigrane
|
||||
watermark.header=Ajouter un filigrane
|
||||
watermark.customColor=Couleur de texte personnalisée
|
||||
watermark.selectText.1=PDF auquel ajouter un filigrane
|
||||
watermark.selectText.2=Texte du filigrane
|
||||
watermark.selectText.3=Taille de police
|
||||
watermark.selectText.4=Rotation (de 0 à 360 degrés)
|
||||
watermark.selectText.5=Width Spacer (espace entre chaque filigrane horizontalement)
|
||||
watermark.selectText.6=Height Spacer (espace entre chaque filigrane verticalement)
|
||||
watermark.selectText.5=widthSpacer (espace entre chaque filigrane horizontalement)
|
||||
watermark.selectText.6=heightSpacer (espace entre chaque filigrane verticalement)
|
||||
watermark.selectText.7=Opacité (de 0% à 100%)
|
||||
watermark.selectText.8=Type de filigrane
|
||||
watermark.selectText.9=Image du filigrane
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Modifier
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF en PDF/A
|
||||
pdfToPDFA.header=PDF en PDF/A
|
||||
pdfToPDFA.credit=Ce service utilise qpdf pour la conversion en PDF/A.
|
||||
pdfToPDFA.credit=Ce service utilise ghostscript pour la conversion en PDF/A.
|
||||
pdfToPDFA.submit=Convertir
|
||||
pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois
|
||||
pdfToPDFA.outputFormat=Format de sortie
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Niveau de Signet : Choisissez le niveau de signets à uti
|
||||
splitByChapters.desc.3=Inclure les Métadonnées : Si coché, les métadonnées du PDF original seront incluses dans chaque PDF divisé.
|
||||
splitByChapters.desc.4=Autoriser les Doublons : Si coché, permet à plusieurs signets sur la même page de créer des PDF séparés.
|
||||
splitByChapters.submit=Diviser le PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Privacy Policy
|
||||
legal.terms=Terms and Conditions
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Replace-Invert Color PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Deisiúchán
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Mód OCR
|
||||
ocr.selectText.11=Bain íomhánna tar éis OCR (Bain GACH íomhá, ní úsáideach ach amháin má tá siad mar chuid den chéim tiontaithe)
|
||||
ocr.selectText.12=Cineál Rindreála (Ardleibhéal)
|
||||
ocr.help=Léigh le do thoil an doiciméadú seo ar conas é seo a úsáid do theangacha eile agus/nó úsáid nach bhfuil i ndugairí
|
||||
ocr.credit=Úsáideann an tseirbhís seo qpdf agus Tesseract le haghaidh OCR.
|
||||
ocr.credit=Úsáideann an tseirbhís seo OCRmyPDF agus Tesseract le haghaidh OCR.
|
||||
ocr.submit=Próiseáil PDF le OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Tiontaigh go PDF
|
||||
#compress
|
||||
compress.title=Comhbhrúigh
|
||||
compress.header=Comhbhrúigh PDF
|
||||
compress.credit=Úsáideann an tseirbhís seo qpdf le haghaidh Comhbhrú/Optimization PDF.
|
||||
compress.credit=Úsáideann an tseirbhís seo Ghostscript le haghaidh Comhbhrú/Optimization PDF.
|
||||
compress.selectText.1=Mód Láimhe - Ó 1 go 4
|
||||
compress.selectText.2=Leibhéal optamaithe:
|
||||
compress.selectText.3=4 (Uafásach le haghaidh íomhánna téacs)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,12 +1034,11 @@ addPassword.submit=Criptigh
|
||||
#watermark
|
||||
watermark.title=Cuir Uisce leis
|
||||
watermark.header=Cuir Uisce leis
|
||||
watermark.customColor=Dath Téacs Saincheaptha
|
||||
watermark.selectText.1=Roghnaigh PDF chun comhartha uisce a chur leis:
|
||||
watermark.selectText.2=Téacs Comhartha Uisce:
|
||||
watermark.selectText.3=Méid cló:
|
||||
watermark.selectText.4=Rothlú (0-360):
|
||||
watermark.selectText.5=Width Spacer (Spás idir gach comhartha uisce go cothrománach):
|
||||
watermark.selectText.5=widthSpacer (Spás idir gach comhartha uisce go cothrománach):
|
||||
watermark.selectText.6=spásaire airde (Spás idir gach comhartha uisce go hingearach):
|
||||
watermark.selectText.7=Teimhneacht (0% - 100%):
|
||||
watermark.selectText.8=Cineál Comhartha Uisce:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Athrú
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF Go PDF/A
|
||||
pdfToPDFA.header=PDF Go PDF/A
|
||||
pdfToPDFA.credit=Úsáideann an tseirbhís seo qpdf chun PDF/A a thiontú
|
||||
pdfToPDFA.credit=Úsáideann an tseirbhís seo ghostscript chun PDF/A a thiontú
|
||||
pdfToPDFA.submit=Tiontaigh
|
||||
pdfToPDFA.tip=Faoi láthair ní oibríonn sé le haghaidh ionchuir iolracha ag an am céanna
|
||||
pdfToPDFA.outputFormat=Formáid aschuir
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||
splitByChapters.submit=Split PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=पृष्ठ
|
||||
pages=पृष्ठों
|
||||
loading=डालिंग...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=गुप्तता सूचना
|
||||
legal.terms=शर्तें और प्रवाह
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=अध्यायों पर अलग-कर
|
||||
home.splitPdfByChapters.desc=पुस्तक के अध्याय की संरचना पर आधारित एक PDF को बहिन-भागों में विभाजित करें
|
||||
splitPdfByChapters.tags=विभाजन,अध्याय,पसंदीदा,रजैत
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=चित्र रंग परिवर्तन/उलटकर परिवर्तन PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=प्रदर्शन बचाएं
|
||||
sign.personalSigs=मौजूदा प्रदर्शन
|
||||
sign.sharedSigs=साझेदार प्रदर्शन
|
||||
sign.noSavedSigs=कोई भी संवर्तित प्रदर्शन नहीं मौजूद है
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=मरम्मत
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR मोड
|
||||
ocr.selectText.11=OCR के बाद छवियां हटाएँ (सभी छवियां हटाएँ, केवल परिवर्तन चरण का हिस्सा होता है)
|
||||
ocr.selectText.12=रेंडर टाइप (उन्नत)
|
||||
ocr.help=कृपया इस डॉक्यूमेंटेशन को पढ़ें कि इसे अन्य भाषाओं के लिए कैसे उपयोग किया जाता है और/या डॉकर में नहीं हैं
|
||||
ocr.credit=इस सेवा में qpdf और टेसरेक्ट का उपयोग होता है।
|
||||
ocr.credit=इस सेवा में OCRmyPDF और टेसरेक्ट का उपयोग होता है।
|
||||
ocr.submit=OCR के साथ PDF प्रोसेस करें
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=पीडीएफ़ में बदलें
|
||||
#compress
|
||||
compress.title=संकुचित करें
|
||||
compress.header=PDF को संकुचित करें
|
||||
compress.credit=यह सेवा PDF संकुचन/अनुकूलन के लिए qpdf का उपयोग करती है।
|
||||
compress.credit=यह सेवा PDF संकुचन/अनुकूलन के लिए Ghostscript का उपयोग करती है।
|
||||
compress.selectText.1=मैनुअल मोड - 1 से 4 तक
|
||||
compress.selectText.2=अनुकूलन स्तर:
|
||||
compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=एन्क्रिप्ट करें
|
||||
#watermark
|
||||
watermark.title=वॉटरमार्क जोड़ें
|
||||
watermark.header=वॉटरमार्क जोड़ें
|
||||
watermark.customColor=संवैधित टेक्स्ट रंग
|
||||
watermark.selectText.1=वॉटरमार्क जोड़ने के लिए पीडीएफ चुनें:
|
||||
watermark.selectText.2=वॉटरमार्क टेक्स्ट:
|
||||
watermark.selectText.3=फ़ॉन्ट साइज़:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=बदलें
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF से PDF/A में
|
||||
pdfToPDFA.header=PDF से PDF/A में
|
||||
pdfToPDFA.credit=इस सेवा में PDF/A परिवर्तन के लिए qpdf का उपयोग किया जाता है।
|
||||
pdfToPDFA.credit=इस सेवा में PDF/A परिवर्तन के लिए ghostscript का उपयोग किया जाता है।
|
||||
pdfToPDFA.submit=परिवर्तित करें
|
||||
pdfToPDFA.tip=यह सैकड़ों प्रविष्टियाँ एक ही समय में काम करते हैं
|
||||
pdfToPDFA.outputFormat=आउटपुट फॉर्मेट
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=लेमैक्स स्तर: विभाजन
|
||||
splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा।
|
||||
splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है।
|
||||
splitByChapters.submit=PDF विभाजित
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Stranica
|
||||
pages=Stranice
|
||||
loading=Učitavanje...
|
||||
addToDoc=Dodaj u dokument
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Politika privatnosti
|
||||
legal.terms=Uspe sodržine
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Podijeli PDF prema glavama
|
||||
home.splitPdfByChapters.desc=Podijeli PDF na više datoteka prema njegovom strukturnom obliku glava.
|
||||
splitPdfByChapters.tags=podjela, glave, markere, organizacija
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Zameni-inverziranje boja u PDF-u
|
||||
@@ -822,12 +817,7 @@ sign.save=Sačuvaj potpisnu oznaku
|
||||
sign.personalSigs=Osobni potpisi
|
||||
sign.sharedSigs=Dijeljeni potpisi
|
||||
sign.noSavedSigs=Nema sacuvanih potpisa pronađenih
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Popravi
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR način
|
||||
ocr.selectText.11=Ukloni slike nakon OCR-a (Uklanja SVE slike, korisno samo ako je dio koraka konverzije)
|
||||
ocr.selectText.12=Vrsta iscrtavanja (napredno)
|
||||
ocr.help=Pročitajte ovu dokumentaciju o tome kako ovo koristiti za druge jezike i/ili koristiti ne u dockeru
|
||||
ocr.credit=Ova usluga koristi qpdf i Tesseract za OCR.
|
||||
ocr.credit=Ova usluga koristi OCRmyPDF i Tesseract za OCR.
|
||||
ocr.submit=Obradi PDF sa OCR-om
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Pretvori u PDF
|
||||
#compress
|
||||
compress.title=Komprimirajte
|
||||
compress.header=Komprimirajte PDF
|
||||
compress.credit=Ova usluga koristi qpdf za komprimiranje / optimizaciju PDF-a.
|
||||
compress.credit=Ova usluga koristi Ghostscript za komprimiranje / optimizaciju PDF-a.
|
||||
compress.selectText.1=Ručni režim - Od 1 do 4
|
||||
compress.selectText.2=Nivo optimizacije:
|
||||
compress.selectText.3=4 (Užasno za tekstualne slike)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Šifriraj
|
||||
#watermark
|
||||
watermark.title=Dodaj vodeni žig
|
||||
watermark.header=Dodaj vodeni žig
|
||||
watermark.customColor=Prilagođena boja teksta
|
||||
watermark.selectText.1=Izaberite PDF za dodavanje vodenog žiga:
|
||||
watermark.selectText.2=Tekst vodenog žiga:
|
||||
watermark.selectText.3=Veličina fonta:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Promijeniti
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF u PDF/A
|
||||
pdfToPDFA.header=PDF u PDF/A
|
||||
pdfToPDFA.credit=Ova usluga koristi qpdf za PDF/A pretvorbu
|
||||
pdfToPDFA.credit=Ova usluga koristi ghostscript za PDF/A pretvorbu
|
||||
pdfToPDFA.submit=Pretvoriti
|
||||
pdfToPDFA.tip=Trenutno ne radi za više unosa odjednom
|
||||
pdfToPDFA.outputFormat=Izlazni format
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Nivo oznaka: Odaberite nivo oznaka koji će se koristiti
|
||||
splitByChapters.desc.3=Uključi metapodatke: Ako je pokušano, metapodaci iz originalne PDF datoteke će biti uključeni u svaku podijeljenu PDF datoteku.
|
||||
splitByChapters.desc.4=Dopuštaj duplikate: Ako je ova opcija zaštićena, dozvoljava se da se na istoj strani mogu stvoriti posebne PDF datoteke s više oznaka.
|
||||
splitByChapters.submit=Podijeli PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Oldal
|
||||
pages=Oldalak
|
||||
loading=Betöltés...
|
||||
addToDoc=Hozzáadás dokumentumba
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Adatvédelmi nyilatkozat
|
||||
legal.terms=Feltételek és feltételek
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=PDF felosztása fejezetek szerint
|
||||
home.splitPdfByChapters.desc=Fejezetei alapján egy PDF fájl több dokumentumba osztás.
|
||||
splitPdfByChapters.tags=Osztás, fejezetek, jelezes, organizálás
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Visszaalakítás-összevétel a színekkel PDF-ben
|
||||
@@ -822,12 +817,7 @@ sign.save=Aláíráshoz mentés
|
||||
sign.personalSigs=Személyi aláíráshoz
|
||||
sign.sharedSigs=Megosztott aláíráshoz
|
||||
sign.noSavedSigs=Nincsenek mentett aláírások találat
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Javítás
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR mód
|
||||
ocr.selectText.11=Képek eltávolítása OCR után (Az ÖSSZES kép eltávolítása, csak akkor hasznos, ha a konverzió része)
|
||||
ocr.selectText.12=Render típusa (Speciális)
|
||||
ocr.help=Kérjük, olvassa el ezt a dokumentációt az egyéb nyelvek használatához és/vagy a nem Docker-es használathoz.
|
||||
ocr.credit=Ez a szolgáltatás az qpdf és a Tesseract OCR használatával működik.
|
||||
ocr.credit=Ez a szolgáltatás az OCRmyPDF és a Tesseract OCR használatával működik.
|
||||
ocr.submit=PDF feldolgozása OCR-rel
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Konvertálás PDF dokumentummá
|
||||
#compress
|
||||
compress.title=Tömörítés
|
||||
compress.header=PDF tömörítése
|
||||
compress.credit=Ez a szolgáltatás a qpdf-et használja a PDF tömörítéséhez/optimalizálásához.
|
||||
compress.credit=Ez a szolgáltatás a Ghostscript-et használja a PDF tömörítéséhez/optimalizálásához.
|
||||
compress.selectText.1=Kézi mód - 1-től 4-ig
|
||||
compress.selectText.2=Optimalizálási szint:
|
||||
compress.selectText.3=4 (nem ajánlott a szöveges képekhez)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,13 +1034,12 @@ addPassword.submit=Titkosítás
|
||||
#watermark
|
||||
watermark.title=Vízjel hozzáadása
|
||||
watermark.header=Vízjel hozzáadása
|
||||
watermark.customColor=Egyéni szövegszín
|
||||
watermark.selectText.1=Válassza ki a PDF-t, amelyhez vízjelet kíván hozzáadni:
|
||||
watermark.selectText.2=Vízjel szövege:
|
||||
watermark.selectText.3=Betűméret:
|
||||
watermark.selectText.4=Forgatás (0-360):
|
||||
watermark.selectText.5=Width Spacer (Hely a vízjelek között vízszintesen):
|
||||
watermark.selectText.6=Height Spacer (Hely a vízjelek között függőlegesen):
|
||||
watermark.selectText.5=widthSpacer (Hely a vízjelek között vízszintesen):
|
||||
watermark.selectText.6=heightSpacer (Hely a vízjelek között függőlegesen):
|
||||
watermark.selectText.7=Átlátszóság (0% - 100%):
|
||||
watermark.selectText.8=Vízjel típusa:
|
||||
watermark.selectText.9=Vízjel képe:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Módosítás
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF >> PDF/A
|
||||
pdfToPDFA.header=PDF >> PDF/A
|
||||
pdfToPDFA.credit=Ez a szolgáltatás az qpdf-t használja a PDF/A konverzióhoz
|
||||
pdfToPDFA.credit=Ez a szolgáltatás az ghostscript-t használja a PDF/A konverzióhoz
|
||||
pdfToPDFA.submit=Konvertálás
|
||||
pdfToPDFA.tip=Jelenleg egyszerre több fájl nem működik ezzel a funkcióval
|
||||
pdfToPDFA.outputFormat=Kimeneti formátum
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Metaadatok belefoglalása: Ha bevanítva van, az eredeti PDF fájl metaadatai megtartódnak minden osztott fájlban.
|
||||
splitByChapters.desc.4=Duplikációk engedélyezése: Ha bevanítva van, lehetővé teszi a megadott oldalon lévő több kijelzőszint alapján új PDF-ek létrehozása.
|
||||
splitByChapters.submit=PDF osztás
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Halaman
|
||||
pages=Halaman-halaman
|
||||
loading=Mengambil data...
|
||||
addToDoc=Tambahkan ke Dokumen
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Kebijakan Privasi
|
||||
legal.terms=Syarat dan Ketentuan
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Pisahkan PDF berdasarkan Bab
|
||||
home.splitPdfByChapters.desc=Memisahkan PDF menjadi beberapa file berdasarkan struktur babnya.
|
||||
splitPdfByChapters.tags=pemisahan,bab,bookmark,atur
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Ganti-Inversi-Warna
|
||||
replace-color.header=Ganti-Inversi Warna PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Simpan Tanda Tangan
|
||||
sign.personalSigs=Tanda Tangan Pribadi
|
||||
sign.sharedSigs=Tanda Tangan Berbagi
|
||||
sign.noSavedSigs=Tidak ditemukan tanda tangan yang disimpan
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Perbaiki
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Mode OCR
|
||||
ocr.selectText.11=Hapus gambar setelah OCR (Menghapus Semua gambar, hanya berguna jika merupakan bagian dari langkah konversi)
|
||||
ocr.selectText.12=Jenis Render (Lanjutan)
|
||||
ocr.help=Silakan baca dokumentasi ini tentang cara menggunakan ini untuk bahasa lain dan/atau penggunaan yang tidak ada di docker
|
||||
ocr.credit=Layanan ini menggunakan qpdf dan Tesseract untuk OCR.
|
||||
ocr.credit=Layanan ini menggunakan OCRmyPDF dan Tesseract untuk OCR.
|
||||
ocr.submit=Memproses PDF dengan OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Konversi ke PDF
|
||||
#compress
|
||||
compress.title=Kompres
|
||||
compress.header=Kompres PDF
|
||||
compress.credit=Layanan ini menggunakan qpdf untuk Kompresi/Optimalisasi PDF.
|
||||
compress.credit=Layanan ini menggunakan Ghostscript untuk Kompresi/Optimalisasi PDF.
|
||||
compress.selectText.1=Mode Manual - Dari 1 hingga 4
|
||||
compress.selectText.2=Tingkat Optimalisasi:
|
||||
compress.selectText.3=4 (Buruk untuk gambar teks)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,13 +1034,12 @@ addPassword.submit=Enkripsi
|
||||
#watermark
|
||||
watermark.title=Tambahkan Watermark
|
||||
watermark.header=Tambahkan Watermark
|
||||
watermark.customColor=Warna Teks Kustom
|
||||
watermark.selectText.1=Pilih PDF untuk menambahkan watermark:
|
||||
watermark.selectText.2=Text Watermark:
|
||||
watermark.selectText.3=Ukuran Huruf:
|
||||
watermark.selectText.4=Rotasi (0-360):
|
||||
watermark.selectText.5=Width Spacer (Spasi diantara setiap watermark horisontal):
|
||||
watermark.selectText.6=Height Spacer (Spasi diantara setiap watermark vertikal):
|
||||
watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal):
|
||||
watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal):
|
||||
watermark.selectText.7=Kejernihan (0% - 100%):
|
||||
watermark.selectText.8=Tipe Watermark:
|
||||
watermark.selectText.9=Gambar Watermark:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Ganti
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF Ke PDF/A
|
||||
pdfToPDFA.header=PDF ke PDF/A
|
||||
pdfToPDFA.credit=Layanan ini menggunakan qpdf untuk konversi PDF/A.
|
||||
pdfToPDFA.credit=Layanan ini menggunakan ghostscript untuk konversi PDF/A.
|
||||
pdfToPDFA.submit=Konversi
|
||||
pdfToPDFA.tip=Saat ini tidak dapat digunakan untuk beberapa input sekaligus
|
||||
pdfToPDFA.outputFormat=Format keluaran
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Tingkatan Markah: Pilih tingkatan markah yang digunakan u
|
||||
splitByChapters.desc.3=Termasuk Metadata: Jika dicentang, metadata asli PDF akan disertakan dalam setiap PDF yang dibagi.
|
||||
splitByChapters.desc.4=Izinkan Duplikat: Jika dicentang, mengizinkan beberapa markah pada halaman yang sama untuk membuat PDF terpisah.
|
||||
splitByChapters.submit=Pecah PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Pagina
|
||||
pages=Pagine
|
||||
loading=Caricamento...
|
||||
addToDoc=Aggiungi al documento
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=Informativa sulla privacy
|
||||
legal.terms=Termini e Condizioni
|
||||
@@ -142,7 +141,7 @@ navbar.language=Lingue
|
||||
navbar.settings=Impostazioni
|
||||
navbar.allTools=Strumenti
|
||||
navbar.multiTool=Strumenti multipli
|
||||
navbar.search=Cerca
|
||||
navbar.search=Search
|
||||
navbar.sections.organize=Organizza
|
||||
navbar.sections.convertTo=Converti in PDF
|
||||
navbar.sections.convertFrom=Converti da PDF
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Dividi PDF per capitoli
|
||||
home.splitPdfByChapters.desc=Dividi un PDF in più file in base alla struttura dei capitoli.
|
||||
splitPdfByChapters.tags=dividi, capitoli, segnalibri, organizza
|
||||
|
||||
home.validateSignature.title=Convalida la firma PDF
|
||||
home.validateSignature.desc=Verificare le firme digitali e i certificati nei documenti PDF
|
||||
validateSignature.tags=firma,verifica,convalida,pdf,certificato,firma digitale,convalida firma,convalida certificato
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Sostituisci-Inverti-Colore
|
||||
replace-color.header=Sostituisci-Inverti colore PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Firma salvata
|
||||
sign.personalSigs=Firme personali
|
||||
sign.sharedSigs=Firme condivise
|
||||
sign.noSavedSigs=Nessuna firma salvata trovata
|
||||
sign.addToAll=Aggiungi a tutte le pagine
|
||||
sign.delete=Elimina
|
||||
sign.first=Prima pagina
|
||||
sign.last=Ultima pagina
|
||||
sign.next=Prossima pagina
|
||||
sign.previous=Pagina precedente
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=Ripara
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=Modalità OCR
|
||||
ocr.selectText.11=Rimuovi immagini dopo la scansione (Rimuove TUTTE le immagini, utile solo come parte del processo di conversione)
|
||||
ocr.selectText.12=Modalità di rendering (avanzato)
|
||||
ocr.help=Per favore leggi la documentazione su come usare il programma per altri linguaggi e/o uso non in Docker
|
||||
ocr.credit=Questo servizio utilizza qpdf e Tesseract per l'OCR.
|
||||
ocr.credit=Questo servizio utilizza OCRmyPDF e Tesseract per l'OCR.
|
||||
ocr.submit=Scansiona testo nel PDF con OCR
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=Converti in PDF
|
||||
#compress
|
||||
compress.title=Comprimi
|
||||
compress.header=Comprimi PDF
|
||||
compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF.
|
||||
compress.credit=Questo servizio utilizza Ghostscript per la compressione/ottimizzazione dei PDF.
|
||||
compress.selectText.1=Modalità manuale - Da 1 a 4
|
||||
compress.selectText.2=Livello di ottimizzazione:
|
||||
compress.selectText.3=4 (Terribile per le immagini di testo)
|
||||
@@ -953,20 +943,8 @@ multiTool.deleteSelected=Elimina selezionata
|
||||
multiTool.downloadAll=Esporta
|
||||
multiTool.downloadSelected=Esporta selezionata
|
||||
|
||||
multiTool.insertPageBreak=Inserisci interruzione di pagina
|
||||
multiTool.addFile=Aggiungi file
|
||||
multiTool.rotateLeft=Ruota a sinistra
|
||||
multiTool.rotateRight=Ruota a destra
|
||||
multiTool.split=Dividi
|
||||
multiTool.moveLeft=Sposta a sinistra
|
||||
multiTool.moveRight=Sposta a destra
|
||||
multiTool.delete=Elimina
|
||||
multiTool.dragDropMessage=Pagina(e) selezionata(e)
|
||||
multiTool.undo=Annulla
|
||||
multiTool.redo=Rifai
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
#view pdf
|
||||
viewPdf.title=Visualizza PDF
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=Crittografa
|
||||
#watermark
|
||||
watermark.title=Aggiungi Filigrana
|
||||
watermark.header=Aggiungi filigrana
|
||||
watermark.customColor=Colore testo personalizzato
|
||||
watermark.selectText.1=Seleziona PDF a cui aggiungere la filigrana:
|
||||
watermark.selectText.2=Testo:
|
||||
watermark.selectText.3=Dimensione carattere:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=Cambia proprietà
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=Da PDF a PDF/A
|
||||
pdfToPDFA.header=Da PDF a PDF/A
|
||||
pdfToPDFA.credit=Questo servizio utilizza qpdf per la conversione in PDF/A.
|
||||
pdfToPDFA.credit=Questo servizio utilizza Ghostscript per la conversione in PDF/A.
|
||||
pdfToPDFA.submit=Converti
|
||||
pdfToPDFA.tip=Attualmente non funziona per più input contemporaneamente
|
||||
pdfToPDFA.outputFormat=Formato di output
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Livello segnalibro: seleziona il livello dei segnalibri d
|
||||
splitByChapters.desc.3=Includi metadati: se selezionato, i metadati del PDF originale verranno inclusi in ogni PDF diviso.
|
||||
splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
|
||||
splitByChapters.submit=Dividi PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Clicca
|
||||
fileChooser.or=o
|
||||
fileChooser.dragAndDrop=Trascina & Rilascia
|
||||
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
|
||||
|
||||
#release notes
|
||||
releases.footer=Rilasci
|
||||
releases.title=Note di rilascio
|
||||
releases.header=Note di rilascio
|
||||
releases.current.version=Rilascio corrente
|
||||
releases.note=Le note di rilascio sono disponibili solo in inglese
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validare le firme PDF
|
||||
validateSignature.header=Convalidare le firme digitali
|
||||
validateSignature.selectPDF=Seleziona il file PDF firmato
|
||||
validateSignature.submit=Convalida firme
|
||||
validateSignature.results=Risultati di convalida
|
||||
validateSignature.status=Stato
|
||||
validateSignature.signer=Firmatario
|
||||
validateSignature.date=Data
|
||||
validateSignature.reason=Ragione
|
||||
validateSignature.location=Posizione
|
||||
validateSignature.noSignatures=Nessuna firma digitale trovata in questo documento
|
||||
validateSignature.status.valid=Valida
|
||||
validateSignature.status.invalid=Invalida
|
||||
validateSignature.chain.invalid=Convalida della catena di certificati non riuscita: impossibile verificare l'identità del firmatario
|
||||
validateSignature.trust.invalid=Certificato non presente nell'archivio attendibile: la fonte non può essere verificata
|
||||
validateSignature.cert.expired=Il certificato è scaduto
|
||||
validateSignature.cert.revoked=Il certificato è stato revocato
|
||||
validateSignature.signature.info=Informazioni sulla firma
|
||||
validateSignature.signature=Firma
|
||||
validateSignature.signature.mathValid=La firma è matematicamente valida MA:
|
||||
validateSignature.selectCustomCert=File di certificato personalizzato X.509 (opzionale)
|
||||
validateSignature.cert.info=Dettagli del certificato
|
||||
validateSignature.cert.issuer=Emittente
|
||||
validateSignature.cert.subject=Soggetto
|
||||
validateSignature.cert.serialNumber=Numero di serie
|
||||
validateSignature.cert.validFrom=Valido da
|
||||
validateSignature.cert.validUntil=Valido fino a
|
||||
validateSignature.cert.algorithm=Algoritmo
|
||||
validateSignature.cert.keySize=Dimensione chiave
|
||||
validateSignature.cert.version=Versione
|
||||
validateSignature.cert.keyUsage=Utilizzo della chiave
|
||||
validateSignature.cert.selfSigned=Autofirmato
|
||||
validateSignature.cert.bits=bit
|
||||
|
||||
@@ -81,7 +81,6 @@ page=Page
|
||||
pages=Pages
|
||||
loading=Loading...
|
||||
addToDoc=Add to Document
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=プライバシーポリシー
|
||||
legal.terms=利用規約
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
|
||||
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
|
||||
splitPdfByChapters.tags=split,chapters,bookmarks,organize
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=Replace-Invert Color PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=Save Signature
|
||||
sign.personalSigs=Personal Signatures
|
||||
sign.sharedSigs=Shared Signatures
|
||||
sign.noSavedSigs=No saved signatures found
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=修復
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCRモード
|
||||
ocr.selectText.11=OCR後に画像を削除する (すべての画像を削除します。変換ステップの一部である場合にのみ有効です)。
|
||||
ocr.selectText.12=レンダリングタイプ (高度)
|
||||
ocr.help=他の言語でこれを使用する方法やDocker以外で使用する方法についてはこのドキュメントをお読みください。
|
||||
ocr.credit=本サービスにはOCRにqpdfとTesseractを使用しています。
|
||||
ocr.credit=本サービスにはOCRにOCRmyPDFとTesseractを使用しています。
|
||||
ocr.submit=OCRでPDFを処理する
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=PDFを変換
|
||||
#compress
|
||||
compress.title=圧縮
|
||||
compress.header=PDFを圧縮
|
||||
compress.credit=本サービスはPDFの圧縮/最適化にqpdfを使用しています。
|
||||
compress.credit=本サービスはPDFの圧縮/最適化にGhostscriptを使用しています。
|
||||
compress.selectText.1=手動モード - 1 から 4
|
||||
compress.selectText.2=品質レベル:
|
||||
compress.selectText.3=4 (テキスト画像は最悪)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=暗号化
|
||||
#watermark
|
||||
watermark.title=透かしの追加
|
||||
watermark.header=透かしの追加
|
||||
watermark.customColor=文字色のカスタム
|
||||
watermark.selectText.1=透かしを追加するPDFを選択:
|
||||
watermark.selectText.2=透かしのテキスト:
|
||||
watermark.selectText.3=文字サイズ:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=変更
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDFをPDF/Aに変換
|
||||
pdfToPDFA.header=PDFをPDF/Aに変換
|
||||
pdfToPDFA.credit=本サービスはPDF/Aの変換にqpdfを使用しています。
|
||||
pdfToPDFA.credit=本サービスはPDF/Aの変換にghostscriptを使用しています。
|
||||
pdfToPDFA.submit=変換
|
||||
pdfToPDFA.tip=現在、一度に複数の入力に対して機能しません
|
||||
pdfToPDFA.outputFormat=Output format
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
|
||||
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
|
||||
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
|
||||
splitByChapters.submit=Split PDF
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
@@ -81,7 +81,6 @@ page=페이지
|
||||
pages=페이지
|
||||
loading=로딩 중...
|
||||
addToDoc=문서에 추가
|
||||
reset=Reset
|
||||
|
||||
legal.privacy=개인 정보 정책
|
||||
legal.terms=이용 약관
|
||||
@@ -512,10 +511,6 @@ home.splitPdfByChapters.title=챕터별로 PDF 분할
|
||||
home.splitPdfByChapters.desc=PDF를 여러 파일로 나눕니다. 각 장의 구조에 따라.
|
||||
splitPdfByChapters.tags=분할, 챕터, 북마크, 조직화
|
||||
|
||||
home.validateSignature.title=Validate PDF Signature
|
||||
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
|
||||
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
|
||||
|
||||
#replace-invert-color
|
||||
replace-color.title=Replace-Invert-Color
|
||||
replace-color.header=색상 교체/반전 PDF
|
||||
@@ -822,12 +817,7 @@ sign.save=서명 저장
|
||||
sign.personalSigs=개인용 서명
|
||||
sign.sharedSigs=공유용 서명
|
||||
sign.noSavedSigs=저장된 서명이 없습니다
|
||||
sign.addToAll=Add to all pages
|
||||
sign.delete=Delete
|
||||
sign.first=First page
|
||||
sign.last=Last page
|
||||
sign.next=Next page
|
||||
sign.previous=Previous page
|
||||
|
||||
|
||||
#repair
|
||||
repair.title=복구
|
||||
@@ -872,7 +862,7 @@ ocr.selectText.10=OCR 모드
|
||||
ocr.selectText.11=OCR 후 이미지 제거(모든 이미지 제거, 변환 단계의 일부인 경우에만 유용)
|
||||
ocr.selectText.12=렌더 유형(고급)
|
||||
ocr.help=다른 언어 또는 Docker에 포함되지 않은 언어에 대해 사용하는 방법에 대해서는 이 문서를 참조합니다.
|
||||
ocr.credit=이 서비스는 OCR에 qpdf와 Tesseract를 사용합니다.
|
||||
ocr.credit=이 서비스는 OCR에 OCRmyPDF와 Tesseract를 사용합니다.
|
||||
ocr.submit=인식
|
||||
|
||||
|
||||
@@ -896,7 +886,7 @@ fileToPDF.submit=PDF로 변환
|
||||
#compress
|
||||
compress.title=압축
|
||||
compress.header=PDF 압축
|
||||
compress.credit=이 서비스는 PDF 압축 및 최적화를 위해 qpdf를 사용합니다.
|
||||
compress.credit=이 서비스는 PDF 압축 및 최적화를 위해 Ghostscript를 사용합니다.
|
||||
compress.selectText.1=수동 모드 - 1에서 4
|
||||
compress.selectText.2=최적화 수준:
|
||||
compress.selectText.3=4 (텍스트 이미지에 적합하지 않음)
|
||||
@@ -953,18 +943,6 @@ multiTool.deleteSelected=Delete Selected
|
||||
multiTool.downloadAll=Export
|
||||
multiTool.downloadSelected=Export Selected
|
||||
|
||||
multiTool.insertPageBreak=Insert Page Break
|
||||
multiTool.addFile=Add File
|
||||
multiTool.rotateLeft=Rotate Left
|
||||
multiTool.rotateRight=Rotate Right
|
||||
multiTool.split=Split
|
||||
multiTool.moveLeft=Move Left
|
||||
multiTool.moveRight=Move Right
|
||||
multiTool.delete=Delete
|
||||
multiTool.dragDropMessage=Page(s) Selected
|
||||
multiTool.undo=Undo
|
||||
multiTool.redo=Redo
|
||||
|
||||
#multiTool-advert
|
||||
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
|
||||
|
||||
@@ -1056,7 +1034,6 @@ addPassword.submit=암호화
|
||||
#watermark
|
||||
watermark.title=워터마크 추가
|
||||
watermark.header=워터마크 추가
|
||||
watermark.customColor=사용자 정의 텍스트 색상
|
||||
watermark.selectText.1=워터마크를 추가할 PDF 선택:
|
||||
watermark.selectText.2=워터마크 텍스트:
|
||||
watermark.selectText.3=폰트 크기:
|
||||
@@ -1119,7 +1096,7 @@ changeMetadata.submit=변경
|
||||
#pdfToPDFA
|
||||
pdfToPDFA.title=PDF를 PDF/A로
|
||||
pdfToPDFA.header=PDF 문서를 PDF/A로 변환
|
||||
pdfToPDFA.credit=이 서비스는 PDF/A 변환을 위해 qpdf 문서를 사용합니다.
|
||||
pdfToPDFA.credit=이 서비스는 PDF/A 변환을 위해 ghostscript 문서를 사용합니다.
|
||||
pdfToPDFA.submit=변환
|
||||
pdfToPDFA.tip=현재 한 번에 여러 입력에 대해 작동하지 않습니다.
|
||||
pdfToPDFA.outputFormat=출력 형식
|
||||
@@ -1267,51 +1244,3 @@ splitByChapters.desc.2=북마크 레벨: 분할에 사용할 북마크 레벨을
|
||||
splitByChapters.desc.3=메타데이터 포함: 체크하면 각 분할된 PDF에는 원본 PDF의 메타데이터가 포함됩니다.
|
||||
splitByChapters.desc.4=중복 허용: 중복 북마크가 있는 같은 페이지에 여러 번 분할 PDF를 생성합니다.
|
||||
splitByChapters.submit=PDF 분할
|
||||
|
||||
#File Chooser
|
||||
fileChooser.click=Click
|
||||
fileChooser.or=or
|
||||
fileChooser.dragAndDrop=Drag & Drop
|
||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||
|
||||
#release notes
|
||||
releases.footer=Releases
|
||||
releases.title=Release Notes
|
||||
releases.header=Release Notes
|
||||
releases.current.version=Current Release
|
||||
releases.note=Release notes are only available in English
|
||||
|
||||
#Validate Signature
|
||||
validateSignature.title=Validate PDF Signatures
|
||||
validateSignature.header=Validate Digital Signatures
|
||||
validateSignature.selectPDF=Select signed PDF file
|
||||
validateSignature.submit=Validate Signatures
|
||||
validateSignature.results=Validation Results
|
||||
validateSignature.status=Status
|
||||
validateSignature.signer=Signer
|
||||
validateSignature.date=Date
|
||||
validateSignature.reason=Reason
|
||||
validateSignature.location=Location
|
||||
validateSignature.noSignatures=No digital signatures found in this document
|
||||
validateSignature.status.valid=Valid
|
||||
validateSignature.status.invalid=Invalid
|
||||
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
|
||||
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
|
||||
validateSignature.cert.expired=Certificate has expired
|
||||
validateSignature.cert.revoked=Certificate has been revoked
|
||||
validateSignature.signature.info=Signature Information
|
||||
validateSignature.signature=Signature
|
||||
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
|
||||
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
|
||||
validateSignature.cert.info=Certificate Details
|
||||
validateSignature.cert.issuer=Issuer
|
||||
validateSignature.cert.subject=Subject
|
||||
validateSignature.cert.serialNumber=Serial Number
|
||||
validateSignature.cert.validFrom=Valid From
|
||||
validateSignature.cert.validUntil=Valid Until
|
||||
validateSignature.cert.algorithm=Algorithm
|
||||
validateSignature.cert.keySize=Key Size
|
||||
validateSignature.cert.version=Version
|
||||
validateSignature.cert.keyUsage=Key Usage
|
||||
validateSignature.cert.selfSigned=Self-Signed
|
||||
validateSignature.cert.bits=bits
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user