Compare commits

..

5 Commits

Author SHA1 Message Date
Anthony Stirling
d19d87b8f0 Update downloader.js 2024-11-20 10:23:29 +00:00
Anthony Stirling
a22ce69bc3 Merge branch 'main' into Frooodle-patch-7 2024-11-20 10:16:44 +00:00
Anthony Stirling
59b60ebfb8 Update common.html 2024-11-20 10:15:23 +00:00
Anthony Stirling
b9a014b5c7 Update downloader.js 2024-11-20 09:23:41 +00:00
Anthony Stirling
9e30918aae Update downloader.js 2024-11-20 09:04:00 +00:00
194 changed files with 4435 additions and 40316 deletions

View File

@@ -1,5 +1,5 @@
blank_issues_enabled: true blank_issues_enabled: true
contact_links: contact_links:
- name: 💬 Discord Server - name: 💬 Discord Server
url: https://discord.gg/HYmhKj45pU url: https://discord.gg/Cn8pWhQRxZ
about: You can join our Discord server for real time discussion and support about: You can join our Discord server for real time discussion and support

View File

@@ -9,9 +9,8 @@ The script also provides functionality to update the translation files to match
adjusting the format. adjusting the format.
Usage: 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 copy
import glob import glob
import os import os
@@ -19,10 +18,6 @@ import argparse
import re import re
# Maximum size for properties files (e.g., 200 KB)
MAX_FILE_SIZE = 200 * 1024
def parse_properties_file(file_path): def parse_properties_file(file_path):
"""Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers).""" """Parses a .properties file and returns a list of objects (including comments, empty lines, and line numbers)."""
properties_list = [] properties_list = []
@@ -100,7 +95,7 @@ def write_json_file(file_path, updated_properties):
def update_missing_keys(reference_file, file_list, branch=""): def update_missing_keys(reference_file, file_list, branch=""):
reference_properties = parse_properties_file(reference_file) reference_properties = parse_properties_file(reference_file)
for file_path in file_list: for file_path in file_list:
basename_current_file = os.path.basename(os.path.join(branch, file_path)) basename_current_file = os.path.basename(branch + file_path)
if ( if (
basename_current_file == os.path.basename(reference_file) basename_current_file == os.path.basename(reference_file)
or not file_path.endswith(".properties") or not file_path.endswith(".properties")
@@ -108,7 +103,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
): ):
continue continue
current_properties = parse_properties_file(os.path.join(branch, file_path)) current_properties = parse_properties_file(branch + file_path)
updated_properties = [] updated_properties = []
for ref_entry in reference_properties: for ref_entry in reference_properties:
ref_entry_copy = copy.deepcopy(ref_entry) ref_entry_copy = copy.deepcopy(ref_entry)
@@ -119,79 +114,60 @@ def update_missing_keys(reference_file, file_list, branch=""):
if ref_entry_copy["key"] == current_entry["key"]: if ref_entry_copy["key"] == current_entry["key"]:
ref_entry_copy["value"] = current_entry["value"] ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy) updated_properties.append(ref_entry_copy)
write_json_file(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): def check_for_missing_keys(reference_file, file_list, branch):
update_missing_keys(reference_file, file_list, branch) update_missing_keys(reference_file, file_list, branch + "/")
def read_properties(file_path): def read_properties(file_path):
if os.path.isfile(file_path) and os.path.exists(file_path): with open(file_path, "r", encoding="utf-8") as file:
with open(file_path, "r", encoding="utf-8") as file: return file.read().splitlines()
return file.read().splitlines()
return [""]
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] reference_branch = reference_file.split("/")[0]
basename_reference_file = os.path.basename(reference_file) basename_reference_file = os.path.basename(reference_file)
report = [] report = []
report.append(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) reference_lines = read_properties(reference_file)
has_differences = False has_differences = False
only_reference_file = True only_reference_file = True
file_arr = file_list for file_path in file_list:
basename_current_file = os.path.basename(branch + "/" + file_path)
if len(file_list) == 1:
file_arr = file_list[0].split()
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
for file_path in file_arr:
absolute_path = os.path.abspath(file_path)
# Verify that file is within the expected directory
if not absolute_path.startswith(base_dir):
raise ValueError(f"Unsafe file found: {file_path}")
# Verify file size before processing
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
raise ValueError(
f"The file {file_path} is too large and could pose a security risk."
)
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if ( if (
basename_current_file == basename_reference_file basename_current_file == basename_reference_file
or not file_path.startswith(
os.path.join("src", "main", "resources", "messages_")
)
or not file_path.endswith(".properties") or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_") or not basename_current_file.startswith("messages_")
): ):
continue continue
only_reference_file = False only_reference_file = False
report.append(f"#### 📃 **File Check:** `{basename_current_file}`") report.append(f"#### 🗂️ **Checking File:** `{basename_current_file}`...")
current_lines = read_properties(os.path.join(branch, file_path)) current_lines = read_properties(branch + "/" + file_path)
reference_line_count = len(reference_lines) reference_line_count = len(reference_lines)
current_line_count = len(current_lines) current_line_count = len(current_lines)
if reference_line_count != current_line_count: if reference_line_count != current_line_count:
report.append("") report.append("")
report.append("1. **Test Status:** ❌ **_Failed_**") report.append("- **Test 1 Status:** ❌ Failed")
report.append(" - **Issue:**")
has_differences = True has_differences = True
if reference_line_count > current_line_count: if reference_line_count > current_line_count:
report.append( report.append(
f" - **_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: elif reference_line_count < current_line_count:
report.append( 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: else:
report.append("1. **Test Status:** ✅ **_Passed_**") report.append("- **Test 1 Status:** ✅ Passed")
# Check for missing or extra keys # Check for missing or extra keys
current_keys = [] current_keys = []
@@ -216,42 +192,32 @@ def check_for_differences(reference_file, file_list, branch, actor):
has_differences = True has_differences = True
missing_keys_str = "`, `".join(missing_keys_list) missing_keys_str = "`, `".join(missing_keys_list)
extra_keys_str = "`, `".join(extra_keys_list) extra_keys_str = "`, `".join(extra_keys_list)
report.append("2. **Test Status:** ❌ **_Failed_**") report.append("- **Test 2 Status:** ❌ Failed")
report.append(" - **Issue:**")
if missing_keys_list: if missing_keys_list:
spaces_keys_list = []
for key in missing_keys_list:
if " " in key:
spaces_keys_list.append(key)
if spaces_keys_list:
spaces_keys_str = "`, `".join(spaces_keys_list)
report.append(
f" - **_Keys containing unnecessary spaces_**: `{spaces_keys_str}`!"
)
report.append( report.append(
f" - **_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: if extra_keys_list:
report.append( 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: 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("---") report.append("---")
report.append("") report.append("")
# update_file_list = glob.glob(branch + "/src/**/messages_*.properties", recursive=True)
# update_missing_keys(reference_file, update_file_list)
# report.append("---")
# report.append("")
if has_differences: if has_differences:
report.append("## ❌ Overall Check Status: **_Failed_**") report.append("## ❌ Overall Check Status: **_Failed_**")
report.append("")
report.append(
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)"
)
else: else:
report.append("## ✅ Overall Check Status: **_Success_**") report.append("## ✅ Overall Check Status: **_Success_**")
report.append("")
report.append(
f"Thanks @{actor} for your help in keeping the translations up to date."
)
if not only_reference_file: if not only_reference_file:
print("\n".join(report)) print("\n".join(report))
@@ -259,11 +225,6 @@ def check_for_differences(reference_file, file_list, branch, actor):
if __name__ == "__main__": if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Find missing keys") parser = argparse.ArgumentParser(description="Find missing keys")
parser.add_argument(
"--actor",
required=False,
help="Actor from PR.",
)
parser.add_argument( parser.add_argument(
"--reference-file", "--reference-file",
required=True, required=True,
@@ -283,21 +244,11 @@ if __name__ == "__main__":
) )
args = parser.parse_args() args = parser.parse_args()
# Sanitize --actor input to avoid injection attacks
if args.actor:
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
# Sanitize --branch input to avoid injection attacks
if args.branch:
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
file_list = args.files file_list = args.files
if file_list is None: if file_list is None:
file_list = glob.glob( file_list = glob.glob(
os.path.join( os.getcwd() + "/src/**/messages_*.properties", recursive=True
os.getcwd(), "src", "main", "resources", "messages_*.properties"
)
) )
update_missing_keys(args.reference_file, file_list) update_missing_keys(args.reference_file, file_list)
else: else:
check_for_differences(args.reference_file, file_list, args.branch, args.actor) check_for_differences(args.reference_file, file_list, args.branch)

View File

@@ -6,10 +6,14 @@ on:
paths: paths:
- "src/main/resources/messages_*.properties" - "src/main/resources/messages_*.properties"
push: push:
branches: ["main"]
paths: paths:
- "src/main/resources/messages_en_GB.properties" - "src/main/resources/messages_en_GB.properties"
# Permissions required for the workflow
permissions:
contents: write
pull-requests: write
jobs: jobs:
check-files: check-files:
if: github.event_name == 'pull_request_target' if: github.event_name == 'pull_request_target'
@@ -50,7 +54,10 @@ jobs:
echo "Getting list of changed files from PR..." 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" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
cd .. cd ..
echo "Setting branch path..."
BRANCH_PATH="pr-branch"
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
echo "Processing changed files..." echo "Processing changed files..."
mapfile -t CHANGED_FILES < changed_files.txt mapfile -t CHANGED_FILES < changed_files.txt
@@ -58,6 +65,7 @@ jobs:
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES_STR}" echo "Changed files: ${CHANGED_FILES_STR}"
echo "Branch: ${BRANCH_PATH}"
- name: Determine reference file - name: Determine reference file
id: determine-file id: determine-file
@@ -79,40 +87,31 @@ jobs:
run: | run: |
echo "Running Python script to check files..." echo "Running Python script to check files..."
python main-branch/.github/scripts/check_language_properties.py \ python main-branch/.github/scripts/check_language_properties.py \
--actor ${{ github.event.pull_request.user.login }} \
--reference-file "${REFERENCE_FILE}" \ --reference-file "${REFERENCE_FILE}" \
--branch pr-branch \ --branch "${BRANCH_PATH}" \
--files "${CHANGED_FILES[@]}" > result.txt || true --files ${CHANGED_FILES} > failure.txt || true
- name: Capture output - name: Capture output
id: capture-output id: capture-output
run: | run: |
if [ -f result.txt ] && [ -s result.txt ]; then if [ -f failure.txt ] && [ -s failure.txt ]; then
echo "Test, capturing output..." echo "Test failed, capturing output..."
SCRIPT_OUTPUT=$(cat result.txt) ERROR_OUTPUT=$(cat failure.txt)
echo "SCRIPT_OUTPUT<<EOF" >> $GITHUB_ENV echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$SCRIPT_OUTPUT" >> $GITHUB_ENV echo "$ERROR_OUTPUT" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
echo "${SCRIPT_OUTPUT}" echo "${ERROR_OUTPUT}"
# Set FAIL_JOB to true if SCRIPT_OUTPUT contains ❌
if [[ "$SCRIPT_OUTPUT" == *"❌"* ]]; then
echo "FAIL_JOB=true" >> $GITHUB_ENV
else
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi
else else
echo "No update found." echo "No errors found."
echo "SCRIPT_OUTPUT=" >> $GITHUB_ENV echo "ERROR_OUTPUT=" >> $GITHUB_ENV
echo "FAIL_JOB=false" >> $GITHUB_ENV
fi fi
- name: Post comment on PR - name: Post comment on PR
if: env.SCRIPT_OUTPUT != '' if: env.ERROR_OUTPUT != ''
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
const { GITHUB_REPOSITORY, SCRIPT_OUTPUT } = process.env; const { GITHUB_REPOSITORY, ERROR_OUTPUT } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = context.issue.number; const prNumber = context.issue.number;
@@ -134,7 +133,7 @@ jobs:
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
comment_id: comment.id, comment_id: comment.id,
body: `## 🚀 Translation Verification Summary\n\n\n${SCRIPT_OUTPUT}\n` body: `## 🚀 Translation Verification Summary\n\n\n${ERROR_OUTPUT}\n`
}); });
console.log("Updated existing comment."); console.log("Updated existing comment.");
} else if (!comment) { } else if (!comment) {
@@ -143,24 +142,33 @@ jobs:
owner: repoOwner, owner: repoOwner,
repo: repoName, repo: repoName,
issue_number: prNumber, 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."); console.log("Created new comment.");
} else { } else {
console.log("Comment update attempt denied. Actor does not match."); console.log("Comment update attempt denied. Actor does not match.");
} }
- name: Fail job if errors found # - name: Set up git config
if: env.FAIL_JOB == 'true' # run: |
run: | # git config --global user.name "github-actions[bot]"
echo "Failing the job because errors were detected." # git config --global user.email "github-actions[bot]@users.noreply.github.com"
exit 1
# - name: Add translation keys
# run: |
# cd ${{ env.BRANCH_PATH }}
# git add src/main/resources/messages_*.properties
# git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
# git commit -m "Update translation files" || echo "No changes to commit"
# - name: Push
# if: env.CHANGES_DETECTED == 'true'
# run: |
# cd pr-branch
# git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.event.pull_request.head.repo.full_name }}.git
# git push origin ${{ github.head_ref }} || echo "Push failed: possibly no changes to push"
update-translations-main: update-translations-main:
if: github.event_name == 'push' if: github.event_name == 'push'
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -192,7 +200,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
if: env.CHANGES_DETECTED == 'true' if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v7 uses: peter-evans/create-pull-request@v6
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update translation files" commit-message: "Update translation files"
@@ -201,8 +209,6 @@ jobs:
signoff: true signoff: true
branch: update_translation_files branch: update_translation_files
title: "Update translation files" title: "Update translation files"
add-paths: |
src/main/resources/messages_*.properties
body: | body: |
Auto-generated by [create-pull-request][1] Auto-generated by [create-pull-request][1]
@@ -210,4 +216,3 @@ jobs:
labels: Translation labels: Translation
draft: false draft: false
delete-branch: true delete-branch: true
sign-commits: true

View File

@@ -1,96 +0,0 @@
name: Test Installers Build
on:
workflow_dispatch:
release:
types: [created]
permissions:
contents: write
packages: write
jobs:
build-installers:
strategy:
matrix:
include:
- os: windows-latest
platform: win
ext: exe
#- os: macos-latest
# platform: mac
# ext: dmg
#- os: ubuntu-latest
# platform: linux
# ext: deb
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.7
# Install Windows dependencies
- name: Install WiX Toolset
if: matrix.os == 'windows-latest'
run: |
curl -L -o wix.exe https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe
.\wix.exe /install /quiet
# Install Linux dependencies
- name: Install Linux Dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y fakeroot rpm
# Get version number
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
- name: Get version number mac
id: versionNumberMac
run: echo "versionNumberMac=$(./gradlew printMacVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
shell: bash
# Build installer
- name: Build Installer
run: ./gradlew build jpackage -x test --info
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: true
# Rename and collect artifacts based on OS
- name: Prepare artifacts
id: prepare
shell: bash
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.exe" "Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
mv "build/jpackage/Stirling-PDF-${{ steps.versionNumberMac.outputs.versionNumberMac }}.dmg" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
else
mv "build/jpackage/stirling-pdf_${{ steps.versionNumber.outputs.versionNumber }}-1_amd64.deb" "Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}-${{ matrix.platform }}.${{ matrix.ext }}"
fi
# Upload installer as artifact for testing
- name: Upload Installer Artifact
uses: actions/upload-artifact@v4
with:
name: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
path: Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./Stirling-PDF-${{ matrix.platform }}-installer.${{ matrix.ext }}

View File

@@ -35,28 +35,27 @@ jobs:
run: ./gradlew clean createExe run: ./gradlew clean createExe
env: env:
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
STIRLING_PDF_DESKTOP_UI: false
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Rename binarie - name: Rename binarie
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe if: matrix.file_suffix != ''
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Upload Assets binarie - name: Upload Assets binarie
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
path: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
name: Stirling-PDF-Server${{ matrix.file_suffix }}.exe name: Stirling-PDF${{ matrix.file_suffix }}.exe
overwrite: true overwrite: true
retention-days: 1 retention-days: 1
if-no-files-found: error if-no-files-found: error
- name: Upload binaries to release - name: Upload binaries to release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./build/launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Rename jar binaries - name: Rename jar binaries
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
@@ -74,43 +73,3 @@ jobs:
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v2
with: with:
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
push-ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v4
with:
gradle-version: 8.7
- name: Generate exe
run: ./gradlew clean createExe
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: true
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Upload Assets binarie
uses: actions/upload-artifact@v4
with:
path: ./build/launch4j/Stirling-PDF.exe
name: Stirling-PDF.exe
overwrite: true
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./build/launch4j/Stirling-PDF.exe

1
.gitignore vendored
View File

@@ -161,4 +161,3 @@ out/
.pytest_cache .pytest_cache
.ipynb_checkpoints .ipynb_checkpoints
**/jcef-bundle/

View File

@@ -11,7 +11,7 @@ Stirling-PDF is built using:
- Spring Boot + Thymeleaf - Spring Boot + Thymeleaf
- PDFBox - PDFBox
- LibreOffice - LibreOffice
- qpdf - OcrMyPdf
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
- Docker - Docker
- PDF.js - PDF.js
@@ -243,7 +243,7 @@ To run Stirling-PDF locally:
Important notes: Important notes:
- Local testing doesn't include features that depend on external tools like 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!) - There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup. - Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.

View File

@@ -30,7 +30,6 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
tini \ tini \
bash \ bash \
curl \ curl \
qpdf \
shadow \ shadow \
su-exec \ su-exec \
openssl \ openssl \
@@ -41,6 +40,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# pdftohtml # pdftohtml
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced features) # OCR MY PDF (unpaper for descew and other advanced features)
ocrmypdf \
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
# CV # CV
py3-opencv \ py3-opencv \

View File

@@ -1,5 +1,5 @@
# Build the application # Build the application
FROM gradle:8.11-jdk17 AS build FROM gradle:8.7-jdk17 AS build
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app
@@ -55,7 +55,7 @@ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /et
# pdftohtml # pdftohtml
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced featues) # OCR MY PDF (unpaper for descew and other advanced featues)
qpdf \ ocrmypdf \
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra \
# CV # CV

View File

@@ -1,5 +1,5 @@
# use alpine # use alpine
FROM alpine:3.21.0 FROM alpine:3.20.3
ARG VERSION_TAG ARG VERSION_TAG

View File

@@ -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 | ✔️ | | | | | | | | | | ✔️ | | | | adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | | | auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | |
@@ -16,7 +16,7 @@
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | | | img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | | | pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | | | pdf-to-img | | ✔️ | | | | ✔️ | | | | ✔️ | | | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | | | pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | ✔️ |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | | | pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | | | pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | | | pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | |
@@ -34,13 +34,13 @@
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | | | auto-rename | | | | ✔️ | | | | | | ✔️ | | | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | | | change-metadata | | | | ✔️ | | | | | | ✔️ | | | |
| compare | | | | ✔️ | | | | | | | ✔️ | | | | compare | | | | ✔️ | | | | | | | ✔️ | | |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | | | compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | ✔️ |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | | | extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | | | extract-images | | | | ✔️ | | | | | | ✔️ | | | |
| flatten | | | | ✔️ | | | | | | | ✔️ | | | | flatten | | | | ✔️ | | | | | | | ✔️ | | |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | | | get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | | | | | | | ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | | | remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | | | repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | ✔️ |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | | | show-javascript | | | | ✔️ | | | | | | | ✔️ | | |
| sign | | | | ✔️ | | | | | | | ✔️ | | | | sign | | | | ✔️ | | | | | | | ✔️ | | |

View File

@@ -8,7 +8,7 @@ The paths have changed for the tessdata locations on new Docker images. Please u
## How does the OCR Work ## How does the OCR Work
Stirling-PDF uses 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 ## Language Packs
@@ -52,6 +52,8 @@ Add the following to your existing Docker run command:
### Non-Docker Setup ### Non-Docker Setup
If you are not using Docker, you need to install the OCR components, including the `ocrmypdf` app. You can see the [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html).
For Debian-based systems, install languages with this command: For Debian-based systems, install languages with this command:
```bash ```bash
@@ -81,7 +83,8 @@ rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
For Windows: For Windows:
You must ensure tesseract is installed Ensure ocrmypdf in installed with
``pip install ocrmypdf``
Additional languages must be downloaded manually: Additional languages must be downloaded manually:
Download desired .traineddata files from tessdata or tessdata_fast Download desired .traineddata files from tessdata or tessdata_fast

View File

@@ -68,7 +68,7 @@ nix-env -iA nixpkgs.jbig2enc
### Step 3: Install Additional Software ### Step 3: Install Additional Software
Next we need to install LibreOffice for conversions, 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: Install the following software:
@@ -81,27 +81,27 @@ Install the following software:
- unoconv - unoconv
- pngquant - pngquant
- unpaper - unpaper
- qpdf - ocrmypdf
- opencv-python-headless - opencv-python-headless
For Debian-based systems, you can use the following command: For Debian-based systems, you can use the following command:
```bash ```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper 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 pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages
``` ```
For Fedora: For Fedora:
```bash ```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper qpdf sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
``` ```
For Nix: For Nix:
```bash ```bash
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.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 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. 1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata` 2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata`
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED. **IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.

View File

@@ -2,11 +2,10 @@
<h1 align="center">Stirling-PDF</h1> <h1 align="center">Stirling-PDF</h1>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](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&#0045;pdf" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=641239&theme=light" alt="Stirling&#0032;PDF - Open&#0032;source&#0032;locally&#0032;hosted&#0032;web&#0032;PDF&#0032;editor | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
[Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements. [Stirling-PDF](https://www.stirlingpdf.com) is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements.
@@ -79,15 +78,15 @@ All files and PDFs exist either exclusively on the client side, reside in server
- Detect and remove blank pages - Detect and remove blank pages
- Compare two PDFs and show differences in text - Compare two PDFs and show differences in text
- Add images to PDFs - Add images to PDFs
- Compress PDFs to decrease their filesize (using qpdf) - Compress PDFs to decrease their filesize (using OCRMyPDF)
- Extract images from PDF - Extract images from PDF
- Remove images from PDF - Remove images from PDF
- Extract images from scans - Extract images from scans
- Remove annotations - Remove annotations
- Add page numbers - Add page numbers
- Auto rename file by detecting PDF header text - Auto rename file by detecting PDF header text
- OCR on PDF (using tesseract) - OCR on PDF (using OCRMyPDF)
- PDF/A conversion (using libreoffice) - PDF/A conversion (using OCRMyPDF)
- Edit metadata - Edit metadata
- Flatten PDFs - Flatten PDFs
- Get all information on a PDF to view or export as JSON - Get all information on a PDF to view or export as JSON
@@ -102,7 +101,7 @@ A demo of the app is available [here](https://stirlingpdf.io).
- Spring Boot + Thymeleaf - Spring Boot + Thymeleaf
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk) - [PDFBox](https://github.com/apache/pdfbox/tree/trunk)
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions - [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [qpdf](https://github.com/qpdf/qpdf) - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
- Docker - Docker
- [PDF.js](https://github.com/mozilla/pdf.js) - [PDF.js](https://github.com/mozilla/pdf.js)
@@ -187,48 +186,46 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
## Supported Languages ## Supported Languages
Stirling-PDF currently supports 38 languages! Stirling-PDF currently supports 36 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) | | Arabic (العربية) (ar_AR) | ![97%](https://geps.dev/progress/97) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) | | Basque (Euskara) (eu_ES) | ![55%](https://geps.dev/progress/55) |
| Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) | | Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
| Bulgarian (Български) (bg_BG) | ![90%](https://geps.dev/progress/90) | | Catalan (Català) (ca_CA) | ![90%](https://geps.dev/progress/90) |
| Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) | | Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) |
| Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) | | Czech (Česky) (cs_CZ) | ![97%](https://geps.dev/progress/97) |
| Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) | | Danish (Dansk) (da_DK) | ![96%](https://geps.dev/progress/96) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) | | Dutch (Nederlands) (nl_NL) | ![96%](https://geps.dev/progress/96) |
| Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) | | French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) | | Greek (Ελληνικά) (el_GR) | ![97%](https://geps.dev/progress/97) |
| Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) | | Hindi (हिंदी) (hi_IN) | ![95%](https://geps.dev/progress/95) |
| Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) | | Hungarian (Magyar) (hu_HU) | ![98%](https://geps.dev/progress/98) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![91%](https://geps.dev/progress/91) | | Indonesian (Bahasa Indonesia) (id_ID) | ![97%](https://geps.dev/progress/97) |
| Irish (Gaeilge) (ga_IE) | ![83%](https://geps.dev/progress/83) | | Irish (Gaeilge) (ga_IE) | ![88%](https://geps.dev/progress/88) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) | | Japanese (日本語) (ja_JP) | ![85%](https://geps.dev/progress/85) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) | | Korean (한국어) (ko_KR) | ![95%](https://geps.dev/progress/95) |
| Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) | | Norwegian (Norsk) (no_NB) | ![87%](https://geps.dev/progress/87) |
| Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) | | Polish (Polski) (pl_PL) | ![97%](https://geps.dev/progress/97) |
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) | | Portuguese (Português) (pt_PT) | ![97%](https://geps.dev/progress/97) |
| Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) | | Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![85%](https://geps.dev/progress/85) | | Romanian (Română) (ro_RO) | ![90%](https://geps.dev/progress/90) |
| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) | | Russian (Русский) (ru_RU) | ![97%](https://geps.dev/progress/97) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![70%](https://geps.dev/progress/70) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) | | Simplified Chinese (简体中文) (zh_CN) | ![91%](https://geps.dev/progress/91) |
| Slovakian (Slovensky) (sk_SK) | ![78%](https://geps.dev/progress/78) | | Slovakian (Slovensky) (sk_SK) | ![82%](https://geps.dev/progress/82) |
| Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) | | Spanish (Español) (es_ES) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![90%](https://geps.dev/progress/90) | | Swedish (Svenska) (sv_SE) | ![97%](https://geps.dev/progress/97) |
| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) | | Thai (ไทย) (th_TH) | ![96%](https://geps.dev/progress/96) |
| Traditional Chinese (繁體中文) (zh_TW) | ![91%](https://geps.dev/progress/91) | | Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) | | Turkish (Türkçe) (tr_TR) | ![92%](https://geps.dev/progress/92) |
| Ukrainian (Українська) (uk_UA) | ![76%](https://geps.dev/progress/76) | | Ukrainian (Українська) (uk_UA) | ![80%](https://geps.dev/progress/80) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) | | Vietnamese (Tiếng Việt) (vi_VN) | ![88%](https://geps.dev/progress/88) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.) ## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
@@ -405,7 +402,7 @@ To access your account settings, go to Account Settings in the settings cog menu
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future. To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user. For API usage, you must provide a header with `X-API-Key` and the associated API key for that user.
## FAQ ## FAQ

View File

@@ -8,7 +8,7 @@ The 'Fat' container contains all those found in 'Full' with security jar along w
| Libre | | ✔️ | | Libre | | ✔️ |
| Python | | ✔️ | | Python | | ✔️ |
| OpenCV | | ✔️ | | OpenCV | | ✔️ |
| qpdf | | ✔️ | | OCRmyPDF | | ✔️ |
| Operation | Ultra-Lite | Full | | Operation | Ultra-Lite | Full |
| ---------------------- | ---------- | ---- | | ---------------------- | ---------- | ---- |

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" 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 "io.spring.dependency-management" version "1.1.6"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0" id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
@@ -8,7 +8,6 @@ plugins {
id "com.diffplug.spotless" version "6.25.0" id "com.diffplug.spotless" version "6.25.0"
id "com.github.jk1.dependency-license-report" version "2.9" id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3" //id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.0"
} }
@@ -16,19 +15,16 @@ plugins {
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
ext { ext {
springBootVersion = "3.4.0" springBootVersion = "3.3.5"
pdfboxVersion = "3.0.3" pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7" logbackVersion = "1.5.7"
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.36"
bouncycastleVersion = "1.79" bouncycastleVersion = "1.78.1"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
} }
group = "stirling.software" group = "stirling.software"
version = "0.36.3" version = "0.33.1"
java { java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21
@@ -42,9 +38,6 @@ repositories {
maven { maven {
url 'https://build.shibboleth.net/maven/releases' url 'https://build.shibboleth.net/maven/releases'
} }
maven { url "https://build.shibboleth.net/maven/releases" }
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
} }
licenseReport { licenseReport {
@@ -68,12 +61,6 @@ sourceSets {
exclude "stirling/software/SPDF/model/User.java" exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**" exclude "stirling/software/SPDF/repository/**"
} }
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
} }
} }
} }
@@ -84,153 +71,16 @@ openApi {
outputFileName = "SwaggerDoc.json" outputFileName = "SwaggerDoc.json"
} }
//0.11.5 to 2024.11.5
def getMacVersion(String version) {
def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
}
jpackage {
input = "build/libs"
appName = "Stirling-PDF"
appVersion = project.version
vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor"
mainJar = "Stirling-PDF-${project.version}.jar"
mainClass = "org.springframework.boot.loader.launch.JarLauncher"
icon = "src/main/resources/static/favicon.ico"
// JVM Options
javaOptions = [
"-DBROWSER_OPEN=true",
"-DSTIRLING_PDF_DESKTOP_UI=true",
"-Djava.awt.headless=false",
"-Dapple.awt.UIElement=true",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED"
]
verbose = true
destination = "${projectDir}/build/jpackage"
// Windows-specific configuration
windows {
launcherAsService = false
appVersion = project.version
winConsole = false
winDirChooser = true
winMenu = true
winShortcut = true
winPerUserInstall = true
winMenuGroup = "Stirling Software"
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
type = "exe"
installDir = "C:/Program Files/Stirling-PDF"
}
// macOS-specific configuration
mac {
appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns"
type = "dmg"
macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially
//installDir = "Applications"
// Add license and other documentation to DMG
/*macDmgContent = [
"README.md",
"LICENSE",
"CHANGELOG.md"
]*/
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
}
// Linux-specific configuration
linux {
appVersion = project.version
icon = "src/main/resources/static/favicon.png"
type = "deb" // Can also use "rpm" for Red Hat-based systems
// Debian package configuration
//linuxPackageName = "stirlingpdf"
linuxDebMaintainer = "support@stirlingpdf.com"
linuxMenuGroup = "Office;PDF;Productivity"
linuxAppCategory = "Office"
linuxAppRelease = "1"
linuxPackageDeps = true
installDir = "/opt/Stirling-PDF"
// RPM-specific settings
//linuxRpmLicenseType = "MIT"
}
// Common additional options
//jLinkOptions = [
// "--strip-debug",
// "--compress=2",
// "--no-header-files",
// "--no-man-pages"
//]
// Add any additional modules required
/*addModules = [
"java.base",
"java.desktop",
"java.logging",
"java.sql",
"java.xml",
"jdk.crypto.ec"
]*/
// Add copyright and license information
copyright = "Copyright © 2024 Stirling Software"
licenseFile = "LICENSE"
}
launch4j { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
outfile="Stirling-PDF.exe" outfile="Stirling-PDF.exe"
headerType="console"
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
headerType = "gui"
} else {
headerType = "console"
}
jarTask = tasks.bootJar jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?" errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"]
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"]
} else {
variables=["BROWSER_OPEN=true"]
}
jreMinVersion="17" jreMinVersion="17"
mutexName="Stirling-PDF" mutexName="Stirling-PDF"
@@ -270,15 +120,8 @@ configurations.all {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat" exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
} }
dependencies { dependencies {
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
implementation "me.friwi:jcefmaven:127.3.1"
implementation "org.openjfx:javafx-controls:21"
implementation "org.openjfx:javafx-swing:21"
}
//security updates //security updates
implementation "org.springframework:spring-webmvc:6.2.0" implementation "org.springframework:spring-webmvc:6.1.14"
implementation("io.github.pixee:java-security-toolkit:1.2.0") implementation("io.github.pixee:java-security-toolkit:1.2.0")
@@ -296,22 +139,22 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.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' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database //2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224"
constraints { constraints {
implementation "org.opensaml:opensaml-core:$openSamlVersion" implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion" implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion" implementation "org.opensaml:opensaml-saml-impl"
} }
implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion" implementation "org.springframework.security:spring-security-saml2-service-provider"
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
implementation 'com.coveo:saml-client:5.0.0' implementation 'com.coveo:saml-client:5.0.0'
@@ -343,7 +186,7 @@ dependencies {
// Image metadata extractor // Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0" implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.18.0" implementation "commons-io:commons-io:2.17.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF //general PDF
@@ -360,19 +203,12 @@ dependencies {
exclude group: "commons-logging", module: "commons-logging" exclude group: "commons-logging", module: "commons-logging"
} }
// https://mvnrepository.com/artifact/technology.tabula/tabula
implementation ('technology.tabula:tabula:1.0.5') {
exclude group: "org.slf4j", module: "slf4j-simple"
exclude group: "org.bouncycastle", module: "bcprov-jdk15on"
exclude group: "com.google.code.gson", module: "gson"
}
implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4' implementation 'org.apache.pdfbox:jbig2-imageio:3.0.4'
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.1" implementation "io.micrometer:micrometer-core:1.13.6"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0" implementation "org.commonmark:commonmark:0.24.0"
@@ -425,14 +261,7 @@ jar {
tasks.named("test") { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion {
doLast {
println project.version
}
}
task printMacVersion { task printVersion {
doLast { println project.version
println getMacVersion(project.version.toString())
}
} }

View File

@@ -48,6 +48,24 @@ Feature: API Validation
And the response status code should be 200 And the response status code should be 200
@ocr @negative
Scenario: Process PDF with text and OCR with type normal
Given I generate a PDF file as "fileInput"
And the pdf contains 3 pages with random text
And the request data includes
| parameter | value |
| languages | eng |
| sidecar | false |
| deskew | true |
| clean | true |
| cleanFinal | true |
| ocrType | Normal |
| ocrRenderType | hocr |
| removeImagesAfter| false |
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
Then the response status code should be 500
@ocr @positive @ocr @positive
Scenario: Process PDF with OCR Scenario: Process PDF with OCR
Given I generate a PDF file as "fileInput" Given I generate a PDF file as "fileInput"
@@ -66,6 +84,26 @@ Feature: API Validation
And the response file should have size greater than 0 And the response file should have size greater than 0
And the response status code should be 200 And the response status code should be 200
@ocr @positive
Scenario: Process PDF with OCR with sidecar
Given I generate a PDF file as "fileInput"
And the request data includes
| parameter | value |
| languages | eng |
| sidecar | true |
| deskew | true |
| clean | true |
| cleanFinal | true |
| ocrType | Force |
| ocrRenderType | hocr |
| removeImagesAfter| false |
When I send the API request to the endpoint "/api/v1/misc/ocr-pdf"
Then the response content type should be "application/octet-stream"
And the response file should have extension ".zip"
And the response ZIP should contain 2 files
And the response file should have size greater than 0
And the response status code should be 200
@libre @positive @libre @positive
Scenario Outline: Convert PDF to various word formats Scenario Outline: Convert PDF to various word formats
@@ -107,7 +145,7 @@ Feature: API Validation
And the response file should have extension ".pdf" And the response file should have extension ".pdf"
And the response file should have size greater than 100 And the response file should have size greater than 100
@compress @qpdf @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput" Given I use an example file at "exampleFiles/ghost3.pdf" as parameter "fileInput"
And the request data includes And the request data includes
@@ -118,7 +156,7 @@ Feature: API Validation
And the response file should have extension ".pdf" And the response file should have extension ".pdf"
And the response file should have size greater than 100 And the response file should have size greater than 100
@compress @qpdf @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput" Given I use an example file at "exampleFiles/ghost2.pdf" as parameter "fileInput"
And the request data includes And the request data includes
@@ -131,7 +169,7 @@ Feature: API Validation
And the response file should have size greater than 100 And the response file should have size greater than 100
@compress @qpdf @positive @compress @ghostscript @positive
Scenario: Compress Scenario: Compress
Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput" Given I use an example file at "exampleFiles/ghost1.pdf" as parameter "fileInput"
And the request data includes And the request data includes

View File

@@ -15,10 +15,6 @@ import shutil
import re import re
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
API_HEADERS = {
'X-API-KEY': '123456789'
}
######### #########
# GIVEN # # GIVEN #
######### #########
@@ -231,7 +227,7 @@ def save_generated_pdf(context, filename):
def step_send_get_request(context, endpoint): def step_send_get_request(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, headers=API_HEADERS) response = requests.get(full_url)
context.response = response context.response = response
@when('I send a GET request to "{endpoint}" with parameters') @when('I send a GET request to "{endpoint}" with parameters')
@@ -239,7 +235,7 @@ def step_send_get_request_with_params(context, endpoint):
base_url = "http://localhost:8080" base_url = "http://localhost:8080"
params = {row['parameter']: row['value'] for row in context.table} params = {row['parameter']: row['value'] for row in context.table}
full_url = f"{base_url}{endpoint}" full_url = f"{base_url}{endpoint}"
response = requests.get(full_url, params=params, headers=API_HEADERS) response = requests.get(full_url, params=params)
context.response = response context.response = response
@when('I send the API request to the endpoint "{endpoint}"') @when('I send the API request to the endpoint "{endpoint}"')
@@ -260,7 +256,7 @@ def step_send_api_request(context, endpoint):
print(f"form_data {file.name} with {mime_type}") print(f"form_data {file.name} with {mime_type}")
form_data.append((key, (file.name, file, mime_type))) form_data.append((key, (file.name, file, mime_type)))
response = requests.post(url, files=form_data, headers=API_HEADERS) response = requests.post(url, files=form_data)
context.response = response context.response = response
######## ########

View File

@@ -1,34 +0,0 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f -H 'X-API-KEY: 123456789' http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
SECURITY_CUSTOMGLOBALAPIKEY: "123456789"
restart: on-failure:5

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@@ -3,11 +3,6 @@ ignore = [
'language.direction', 'language.direction',
] ]
[az_AZ]
ignore = [
'language.direction',
]
[bg_BG] [bg_BG]
ignore = [ ignore = [
'language.direction', 'language.direction',
@@ -42,19 +37,14 @@ ignore = [
'addPageNumbers.selectText.3', 'addPageNumbers.selectText.3',
'alphabet', 'alphabet',
'certSign.name', 'certSign.name',
'fileChooser.dragAndDrop',
'home.pipeline.title', 'home.pipeline.title',
'language.direction', 'language.direction',
'legal.impressum',
'licenses.version', 'licenses.version',
'pipeline.title', 'pipeline.title',
'pipelineOptions.pipelineHeader', 'pipelineOptions.pipelineHeader',
'pro', 'pro',
'sponsor', 'sponsor',
'text', 'text',
'validateSignature.cert.bits',
'validateSignature.cert.version',
'validateSignature.status',
'watermark.type.1', 'watermark.type.1',
] ]
@@ -66,6 +56,7 @@ ignore = [
[es_ES] [es_ES]
ignore = [ ignore = [
'adminUserSettings.roles', 'adminUserSettings.roles',
'color',
'error', 'error',
'language.direction', 'language.direction',
'no', 'no',
@@ -77,11 +68,6 @@ ignore = [
'language.direction', 'language.direction',
] ]
[fa_IR]
ignore = [
'language.direction',
]
[fr_FR] [fr_FR]
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',

View File

@@ -1,8 +1,8 @@
#!/bin/bash #!/bin/bash
translation_key="pdfToPDFA.credit" translation_key="pdfToPDFA.credit"
old_value="qpdf" old_value="OCRmyPDF"
new_value="liibreoffice" new_value="ghostscript"
for file in ../src/main/resources/messages_*.properties; do for file in ../src/main/resources/messages_*.properties; do
sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file" sed -i "/^$translation_key=/s/$old_value/$new_value/" "$file"

View File

@@ -3,14 +3,13 @@ package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered; import org.springframework.context.annotation.Lazy;
import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
@Order(Ordered.HIGHEST_PRECEDENCE) @Lazy
@Slf4j @Slf4j
public class EEAppConfig { public class EEAppConfig {

View File

@@ -25,10 +25,9 @@ public class LicenseKeyChecker {
KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) { KeygenLicenseVerifier licenseService, ApplicationProperties applicationProperties) {
this.licenseService = licenseService; this.licenseService = licenseService;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.checkLicense();
} }
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds @Scheduled(fixedRate = 604800000, initialDelay = 1000) // 7 days in milliseconds
public void checkLicensePeriodically() { public void checkLicensePeriodically() {
checkLicense(); checkLicense();
} }

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.nio.file.Files; import java.nio.file.Files;
@@ -9,9 +8,6 @@ import java.nio.file.Paths;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import javax.swing.*;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -23,16 +19,13 @@ import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import io.github.pixee.security.SystemCommand; import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.UI.WebBrowser;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@SpringBootApplication @SpringBootApplication
@EnableScheduling @EnableScheduling
@Slf4j
public class SPdfApplication { public class SPdfApplication {
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class); private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class);
@@ -74,19 +67,36 @@ public class SPdfApplication {
} }
} }
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = baseUrl + ":" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
}
}
logger.info("Running configs {}", applicationProperties.toString());
}
public static void main(String[] args) throws IOException, InterruptedException { public static void main(String[] args) throws IOException, InterruptedException {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
Properties props = new Properties();
if (Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
System.setProperty("java.awt.headless", "false");
app.setHeadless(false);
props.put("java.awt.headless", "false");
props.put("spring.main.web-application-type", "servlet");
}
app.setAdditionalProfiles("default"); app.setAdditionalProfiles("default");
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); Map<String, String> propertyFiles = new HashMap<>();
@@ -110,20 +120,14 @@ public class SPdfApplication {
} else { } else {
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist."); logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
} }
Properties finalProps = new Properties();
if (!propertyFiles.isEmpty()) { if (!propertyFiles.isEmpty()) {
finalProps.putAll( app.setDefaultProperties(
Collections.singletonMap( Collections.singletonMap(
"spring.config.additional-location", "spring.config.additional-location",
propertyFiles.get("spring.config.additional-location"))); propertyFiles.get("spring.config.additional-location")));
} }
if (!props.isEmpty()) {
finalProps.putAll(props);
}
app.setDefaultProperties(finalProps);
app.run(args); app.run(args);
// Ensure directories are created // Ensure directories are created
@@ -143,46 +147,6 @@ public class SPdfApplication {
logger.info("Navigate to {}", url); logger.info("Navigate to {}", url);
} }
@Autowired(required = false)
private WebBrowser webBrowser;
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
String url = baseUrl + ":" + getStaticPort();
if (webBrowser != null
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
webBrowser.initWebUI(url);
} else {
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
}
}
}
logger.info("Running configs {}", applicationProperties.toString());
}
@PreDestroy
public void cleanup() {
if (webBrowser != null) {
webBrowser.cleanup();
}
}
public static String getStaticBaseUrl() { public static String getStaticBaseUrl() {
return baseUrlStatic; return baseUrlStatic;
} }

View File

@@ -1,7 +0,0 @@
package stirling.software.SPDF.UI;
public interface WebBrowser {
void initWebUI(String url);
void cleanup();
}

View File

@@ -1,354 +0,0 @@
package stirling.software.SPDF.UI.impl;
import java.awt.AWTException;
import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;
import java.io.File;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import javax.imageio.ImageIO;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import org.cef.CefApp;
import org.cef.CefClient;
import org.cef.CefSettings;
import org.cef.browser.CefBrowser;
import org.cef.callback.CefBeforeDownloadCallback;
import org.cef.callback.CefDownloadItem;
import org.cef.callback.CefDownloadItemCallback;
import org.cef.handler.CefDownloadHandlerAdapter;
import org.cef.handler.CefLoadHandlerAdapter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import me.friwi.jcefmaven.CefAppBuilder;
import me.friwi.jcefmaven.EnumProgress;
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
import stirling.software.SPDF.UI.WebBrowser;
@Component
@Slf4j
@ConditionalOnProperty(
name = "STIRLING_PDF_DESKTOP_UI",
havingValue = "true",
matchIfMissing = false)
public class DesktopBrowser implements WebBrowser {
private static CefApp cefApp;
private static CefClient client;
private static CefBrowser browser;
private static JFrame frame;
private static LoadingWindow loadingWindow;
private static volatile boolean browserInitialized = false;
private static TrayIcon trayIcon;
private static SystemTray systemTray;
public DesktopBrowser() {
SwingUtilities.invokeLater(
() -> {
loadingWindow = new LoadingWindow(null, "Initializing...");
loadingWindow.setVisible(true);
});
}
public void initWebUI(String url) {
CompletableFuture.runAsync(
() -> {
try {
CefAppBuilder builder = new CefAppBuilder();
configureCefSettings(builder);
builder.setProgressHandler(createProgressHandler());
// Build and initialize CEF
cefApp = builder.build();
client = cefApp.createClient();
// Set up download handler
setupDownloadHandler();
// Create browser and frame on EDT
SwingUtilities.invokeAndWait(
() -> {
browser = client.createBrowser(url, false, false);
setupMainFrame();
setupLoadHandler();
// Show the frame immediately but transparent
frame.setVisible(true);
});
} catch (Exception e) {
log.error("Error initializing JCEF browser: ", e);
cleanup();
}
});
}
private void configureCefSettings(CefAppBuilder builder) {
CefSettings settings = builder.getCefSettings();
settings.cache_path = new File("jcef-bundle").getAbsolutePath();
settings.root_cache_path = new File("jcef-bundle").getAbsolutePath();
settings.persist_session_cookies = true;
settings.windowless_rendering_enabled = false;
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
builder.setAppHandler(
new MavenCefAppHandlerAdapter() {
@Override
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
log.info("CEF state changed: " + state);
if (state == CefApp.CefAppState.TERMINATED) {
System.exit(0);
}
}
});
}
private void setupDownloadHandler() {
client.addDownloadHandler(
new CefDownloadHandlerAdapter() {
@Override
public boolean onBeforeDownload(
CefBrowser browser,
CefDownloadItem downloadItem,
String suggestedName,
CefBeforeDownloadCallback callback) {
callback.Continue("", true);
return true;
}
@Override
public void onDownloadUpdated(
CefBrowser browser,
CefDownloadItem downloadItem,
CefDownloadItemCallback callback) {
if (downloadItem.isComplete()) {
log.info("Download completed: " + downloadItem.getFullPath());
} else if (downloadItem.isCanceled()) {
log.info("Download canceled: " + downloadItem.getFullPath());
}
}
});
}
private ConsoleProgressHandler createProgressHandler() {
return new ConsoleProgressHandler() {
@Override
public void handleProgress(EnumProgress state, float percent) {
Objects.requireNonNull(state, "state cannot be null");
SwingUtilities.invokeLater(
() -> {
if (loadingWindow != null) {
switch (state) {
case LOCATING:
loadingWindow.setStatus("Locating Files...");
loadingWindow.setProgress(0);
break;
case DOWNLOADING:
if (percent >= 0) {
loadingWindow.setStatus(
String.format(
"Downloading additional files: %.0f%%",
percent));
loadingWindow.setProgress((int) percent);
}
break;
case EXTRACTING:
loadingWindow.setStatus("Extracting files...");
loadingWindow.setProgress(60);
break;
case INITIALIZING:
loadingWindow.setStatus("Initializing UI...");
loadingWindow.setProgress(80);
break;
case INITIALIZED:
loadingWindow.setStatus("Finalising startup...");
loadingWindow.setProgress(90);
break;
}
}
});
}
};
}
private void setupMainFrame() {
frame = new JFrame("Stirling-PDF");
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
frame.setUndecorated(true);
frame.setOpacity(0.0f);
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.setDoubleBuffered(true);
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
frame.setContentPane(contentPane);
frame.addWindowListener(
new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
cleanup();
System.exit(0);
}
});
frame.setSize(1280, 768);
frame.setLocationRelativeTo(null);
loadIcon();
}
private void setupLoadHandler() {
client.addLoadHandler(
new CefLoadHandlerAdapter() {
@Override
public void onLoadingStateChange(
CefBrowser browser,
boolean isLoading,
boolean canGoBack,
boolean canGoForward) {
if (!isLoading && !browserInitialized) {
browserInitialized = true;
SwingUtilities.invokeLater(
() -> {
if (loadingWindow != null) {
Timer timer =
new Timer(
500,
e -> {
loadingWindow.dispose();
loadingWindow = null;
frame.dispose();
frame.setOpacity(1.0f);
frame.setUndecorated(false);
frame.pack();
frame.setSize(1280, 800);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.requestFocus();
frame.toFront();
browser.getUIComponent()
.requestFocus();
});
timer.setRepeats(false);
timer.start();
}
});
}
}
});
}
private void setupTrayIcon(Image icon) {
if (!SystemTray.isSupported()) {
log.warn("System tray is not supported");
return;
}
try {
systemTray = SystemTray.getSystemTray();
// Create popup menu
PopupMenu popup = new PopupMenu();
// Create menu items
MenuItem showItem = new MenuItem("Show");
showItem.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
MenuItem exitItem = new MenuItem("Exit");
exitItem.addActionListener(
e -> {
cleanup();
System.exit(0);
});
// Add menu items to popup menu
popup.add(showItem);
popup.addSeparator();
popup.add(exitItem);
// Create tray icon
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
trayIcon.setImageAutoSize(true);
// Add double-click behavior
trayIcon.addActionListener(
e -> {
frame.setVisible(true);
frame.setState(Frame.NORMAL);
});
// Add tray icon to system tray
systemTray.add(trayIcon);
// Modify frame behavior to minimize to tray
frame.addWindowStateListener(
new WindowStateListener() {
public void windowStateChanged(WindowEvent e) {
if (e.getNewState() == Frame.ICONIFIED) {
frame.setVisible(false);
}
}
});
} catch (AWTException e) {
log.error("Error setting up system tray icon", e);
}
}
private void loadIcon() {
try {
Image icon = null;
String[] iconPaths = {"/static/favicon.ico"};
for (String path : iconPaths) {
if (icon != null) break;
try {
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is != null) {
icon = ImageIO.read(is);
break;
}
}
} catch (Exception e) {
log.debug("Could not load icon from " + path, e);
}
}
if (icon != null) {
frame.setIconImage(icon);
setupTrayIcon(icon);
} else {
log.warn("Could not load icon from any source");
}
} catch (Exception e) {
log.error("Error loading icon", e);
}
}
@PreDestroy
public void cleanup() {
if (browser != null) browser.close(true);
if (client != null) client.dispose();
if (cefApp != null) cefApp.dispose();
if (loadingWindow != null) loadingWindow.dispose();
}
}

View File

@@ -1,114 +0,0 @@
package stirling.software.SPDF.UI.impl;
import java.awt.*;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.*;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LoadingWindow extends JDialog {
private final JProgressBar progressBar;
private final JLabel statusLabel;
private final JPanel mainPanel;
private final JLabel brandLabel;
public LoadingWindow(Frame parent, String initialUrl) {
super(parent, "Initializing Stirling-PDF", true);
// Initialize components
mainPanel = new JPanel();
mainPanel.setBackground(Color.WHITE);
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 30, 20, 30));
mainPanel.setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
// Configure GridBagConstraints
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(5, 5, 5, 5);
gbc.weightx = 1.0; // Add horizontal weight
gbc.weighty = 0.0; // Add vertical weight
// Add icon
try {
try (InputStream is = getClass().getResourceAsStream("/static/favicon.ico")) {
if (is != null) {
Image img = ImageIO.read(is);
if (img != null) {
Image scaledImg = img.getScaledInstance(48, 48, Image.SCALE_SMOOTH);
JLabel iconLabel = new JLabel(new ImageIcon(scaledImg));
iconLabel.setHorizontalAlignment(SwingConstants.CENTER);
gbc.gridy = 0;
mainPanel.add(iconLabel, gbc);
}
}
}
} catch (Exception e) {
log.error("Failed to load icon", e);
}
// URL Label with explicit size
brandLabel = new JLabel(initialUrl);
brandLabel.setHorizontalAlignment(SwingConstants.CENTER);
brandLabel.setPreferredSize(new Dimension(300, 25));
brandLabel.setText("Stirling-PDF");
gbc.gridy = 1;
mainPanel.add(brandLabel, gbc);
// Status label with explicit size
statusLabel = new JLabel("Initializing...");
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
statusLabel.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 2;
mainPanel.add(statusLabel, gbc);
// Progress bar with explicit size
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setPreferredSize(new Dimension(300, 25));
gbc.gridy = 3;
mainPanel.add(progressBar, gbc);
// Set dialog properties
setContentPane(mainPanel);
setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
setResizable(false);
setUndecorated(false);
// Set size and position
setSize(400, 200);
setLocationRelativeTo(parent);
setAlwaysOnTop(true);
setProgress(0);
setStatus("Starting...");
}
public void setProgress(final int progress) {
SwingUtilities.invokeLater(
() -> {
try {
progressBar.setValue(Math.min(Math.max(progress, 0), 100));
progressBar.setString(progress + "%");
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating progress", e);
}
});
}
public void setStatus(final String status) {
log.info(status);
SwingUtilities.invokeLater(
() -> {
try {
statusLabel.setText(status != null ? status : "");
mainPanel.revalidate();
mainPanel.repaint();
} catch (Exception e) {
log.error("Error updating status", e);
}
});
}
}

View File

@@ -188,7 +188,7 @@ public class EndpointConfiguration {
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
// LibreOffice // LibreOffice
addEndpointToGroup("qpdf", "repair"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
@@ -199,11 +199,10 @@ public class EndpointConfiguration {
// Unoconv // Unoconv
addEndpointToGroup("Unoconv", "file-to-pdf"); addEndpointToGroup("Unoconv", "file-to-pdf");
// qpdf // OCRmyPDF
addEndpointToGroup("qpdf", "compress-pdf"); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
addEndpointToGroup("OCRmyPDF", "ocr-pdf");
addEndpointToGroup("tesseract", "ocr-pdf");
// Java // Java
addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "merge-pdfs");
@@ -249,10 +248,10 @@ public class EndpointConfiguration {
addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Javascript", "adjust-contrast"); addEndpointToGroup("Javascript", "adjust-contrast");
// qpdf dependent endpoints // Ghostscript dependent endpoints
addEndpointToGroup("qpdf", "compress-pdf"); addEndpointToGroup("Ghostscript", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa"); addEndpointToGroup("Ghostscript", "pdf-to-pdfa");
addEndpointToGroup("qpdf", "repair"); addEndpointToGroup("Ghostscript", "repair");
// Weasyprint dependent endpoints // Weasyprint dependent endpoints
addEndpointToGroup("Weasyprint", "html-to-pdf"); addEndpointToGroup("Weasyprint", "html-to-pdf");
@@ -260,9 +259,6 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints // Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html"); addEndpointToGroup("Pdftohtml", "pdf-to-html");
// disabled for now while we resolve issues
disableEndpoint("pdf-to-pdfa");
} }
private void processEnvironmentConfigs() { private void processEnvironmentConfigs() {

View File

@@ -37,12 +37,12 @@ public class ExternalAppDepConfig {
private final Map<String, List<String>> commandToGroupMapping = private final Map<String, List<String>> commandToGroupMapping =
new HashMap<>() { new HashMap<>() {
{ {
put("gs", List.of("Ghostscript"));
put("soffice", List.of("LibreOffice")); put("soffice", List.of("LibreOffice"));
put("ocrmypdf", List.of("OCRmyPDF"));
put("weasyprint", List.of("Weasyprint")); put("weasyprint", List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml")); put("pdftohtml", List.of("Pdftohtml"));
put("unoconv", List.of("Unoconv")); put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract"));
} }
}; };
@@ -97,9 +97,9 @@ public class ExternalAppDepConfig {
public void checkDependencies() { public void checkDependencies() {
// Check core dependencies // Check core dependencies
checkDependencyAndDisableGroup("tesseract"); checkDependencyAndDisableGroup("gs");
checkDependencyAndDisableGroup("soffice"); checkDependencyAndDisableGroup("soffice");
checkDependencyAndDisableGroup("qpdf"); checkDependencyAndDisableGroup("ocrmypdf");
checkDependencyAndDisableGroup("weasyprint"); checkDependencyAndDisableGroup("weasyprint");
checkDependencyAndDisableGroup("pdftohtml"); checkDependencyAndDisableGroup("pdftohtml");
checkDependencyAndDisableGroup("unoconv"); checkDependencyAndDisableGroup("unoconv");

View File

@@ -1,14 +1,11 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import java.io.IOException;
import java.util.Properties;
import java.util.UUID; import java.util.UUID;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.StringUtils;
@@ -26,18 +23,6 @@ public class InitialSetup {
@Autowired private ApplicationProperties applicationProperties; @Autowired private ApplicationProperties applicationProperties;
@PostConstruct @PostConstruct
public void init() throws IOException {
initUUIDKey();
initSecretKey();
initEnableCSRFSecurity();
initLegalUrls();
initSetAppVersion();
}
public void initUUIDKey() throws IOException { public void initUUIDKey() throws IOException {
String uuid = applicationProperties.getAutomaticallyGenerated().getUUID(); String uuid = applicationProperties.getAutomaticallyGenerated().getUUID();
if (!GeneralUtils.isValidUUID(uuid)) { if (!GeneralUtils.isValidUUID(uuid)) {
@@ -47,6 +32,7 @@ public class InitialSetup {
} }
} }
@PostConstruct
public void initSecretKey() throws IOException { public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (!GeneralUtils.isValidUUID(secretKey)) { if (!GeneralUtils.isValidUUID(secretKey)) {
@@ -56,24 +42,13 @@ public class InitialSetup {
} }
} }
public void initEnableCSRFSecurity() throws IOException { @PostConstruct
if (GeneralUtils.isVersionHigher(
"0.36.0", applicationProperties.getAutomaticallyGenerated().getAppVersion())) {
Boolean csrf = applicationProperties.getSecurity().getCsrfDisabled();
if (!csrf) {
GeneralUtils.saveKeyToConfig("security.csrfDisabled", false, false);
GeneralUtils.saveKeyToConfig("system.enableAnalytics", "true", false);
applicationProperties.getSecurity().setCsrfDisabled(false);
}
}
}
public void initLegalUrls() throws IOException { public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions // Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions(); String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) { if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions"; String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl, false); GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl); applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
} }
@@ -81,23 +56,8 @@ public class InitialSetup {
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy(); String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) { if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy"; String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl, false); GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl); applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
} }
} }
public void initSetAppVersion() throws IOException {
String appVersion = "0.0.0";
Resource resource = new ClassPathResource("version.properties");
Properties props = new Properties();
try {
props.load(resource.getInputStream());
appVersion = props.getProperty("version");
} catch (Exception e) {
}
applicationProperties.getAutomaticallyGenerated().setAppVersion(appVersion);
GeneralUtils.saveKeyToConfig("AutomaticallyGenerated.appVersion", appVersion, false);
}
} }

View File

@@ -30,7 +30,6 @@ public class InitialSecuritySetup {
initializeAdminUser(); initializeAdminUser();
} else { } else {
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO();
} }
initializeInternalApiUser(); initializeInternalApiUser();
} }
@@ -75,6 +74,5 @@ public class InitialSecuritySetup {
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
} }
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
} }
} }

View File

@@ -3,16 +3,14 @@ package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.ProviderManager; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -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.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
@@ -44,7 +41,6 @@ import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@@ -68,7 +64,6 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
@Slf4j @Slf4j
@DependsOn("runningEE")
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
@@ -84,10 +79,6 @@ public class SecurityConfiguration {
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter; @Autowired private UserAuthenticationFilter userAuthenticationFilter;
@@ -99,14 +90,13 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (!applicationProperties.getSecurity().getCsrfDisabled()) { if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@@ -116,7 +106,7 @@ public class SecurityConfiguration {
csrf -> csrf ->
csrf.ignoringRequestMatchers( csrf.ignoringRequestMatchers(
request -> { request -> {
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
// If there's no API key, don't ignore CSRF // If there's no API key, don't ignore CSRF
// (return false) // (return false)
@@ -255,22 +245,12 @@ public class SecurityConfiguration {
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE if (applicationProperties.getSecurity().isSaml2Activ()
// Configure the authentication provider && applicationProperties.getSystem().getEnableAlphaFunctionality()) {
OpenSaml4AuthenticationProvider authenticationProvider = http.authenticationProvider(samlAuthenticationProvider());
new OpenSaml4AuthenticationProvider(); http.saml2Login(
authenticationProvider.setResponseAuthenticationConverter( saml2 ->
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
.saml2Login(
saml2 -> {
try {
saml2.loginPage("/saml2") saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrations())
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
@@ -278,34 +258,44 @@ public class SecurityConfiguration {
userService)) userService))
.failureHandler( .failureHandler(
new CustomSaml2AuthenticationFailureHandler()) new CustomSaml2AuthenticationFailureHandler())
.authenticationRequestResolver( .permitAll())
authenticationRequestResolver( .addFilterBefore(
relyingPartyRegistrations())); userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e);
}
});
} }
} else { } else {
// if (!applicationProperties.getSecurity().getCsrfDisabled()) { if (applicationProperties.getSecurity().getCsrfDisabled()) {
// CookieCsrfTokenRepository cookieRepo = http.csrf(csrf -> csrf.disable());
// CookieCsrfTokenRepository.withHttpOnlyFalse(); } else {
// CsrfTokenRequestAttributeHandler requestHandler = CookieCsrfTokenRepository cookieRepo =
// new CsrfTokenRequestAttributeHandler(); CookieCsrfTokenRepository.withHttpOnlyFalse();
// requestHandler.setCsrfRequestAttributeName(null); CsrfTokenRequestAttributeHandler requestHandler =
// http.csrf( new CsrfTokenRequestAttributeHandler();
// csrf -> requestHandler.setCsrfRequestAttributeName(null);
// csrf.csrfTokenRepository(cookieRepo) http.csrf(
// .csrfTokenRequestHandler(requestHandler)); csrf ->
// } csrf.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
} }
return http.build(); return http.build();
} }
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
return authenticationProvider;
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
value = "security.oauth2.enabled", value = "security.oauth2.enabled",
@@ -442,12 +432,11 @@ public class SecurityConfiguration {
havingValue = "true", havingValue = "true",
matchIfMissing = false) matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey(); Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert(); Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential = Saml2X509Credential signingCredential =
@@ -456,97 +445,26 @@ public class SecurityConfiguration {
CertificateUtils.readCertificate(certificateResource), CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING); Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp = RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials(c -> c.add(signingCredential)) .signingX509Credentials((c) -> c.add(signingCredential))
.assertingPartyMetadata( .assertingPartyDetails(
metadata -> (details) ->
metadata.entityId(samlConf.getIdpIssuer()) details.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation( .singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl()) samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials( .verificationX509Credentials(
c -> c.add(verificationCredential)) (c) -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true)) .wantAuthnRequestsSigned(true))
.build(); .build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
} }
@Bean @Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() { public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); provider.setUserDetailsService(userDetailsService);

View File

@@ -71,7 +71,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
try { try {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication

View File

@@ -59,7 +59,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
String identifier = null; String identifier = null;
// Check for API key in the request headers // Check for API key in the request headers
String apiKey = request.getHeader("X-API-KEY"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
identifier = identifier =
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
@@ -79,7 +79,7 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
Role userRole = Role userRole =
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-KEY") != null) { if (request.getHeader("X-API-Key") != null) {
// It's an API call // It's an API call
processRequest( processRequest(
userRole.getApiCallsPerDay(), userRole.getApiCallsPerDay(),

View File

@@ -18,7 +18,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
@@ -51,19 +50,8 @@ public class UserService implements UserServiceInterface {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Transactional
public void migrateOauth2ToSSO() {
userRepository
.findByAuthenticationTypeIgnoreCase("OAUTH2")
.forEach(
user -> {
user.setAuthenticationType(AuthenticationType.SSO);
userRepository.save(user);
});
}
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processSSOPostLogin(String username, boolean autoCreateUser) public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
@@ -73,7 +61,7 @@ public class UserService implements UserServiceInterface {
return true; return true;
} }
if (autoCreateUser) { if (autoCreateUser) {
saveUser(username, AuthenticationType.SSO); saveUser(username, AuthenticationType.OAUTH2);
return true; return true;
} }
return false; return false;
@@ -390,37 +378,6 @@ public class UserService implements UserServiceInterface {
} }
} }
@Transactional
public void syncCustomApiUser(String customApiKey) throws IOException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
return;
}
String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
databaseBackupHelper.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
}
}
}
@Override @Override
public long getTotalUsersCount() { public long getTotalUsersCount() {
return userRepository.count(); return userRepository.count();

View File

@@ -82,7 +82,8 @@ public class CustomOAuth2AuthenticationSuccessHandler
} }
if (userService.usernameExistsIgnoreCase(username) if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username) && userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(username, AuthenticationType.SSO) && !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& oAuth.getAutoCreateUser()) { && oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
@@ -94,7 +95,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
return; return;
} }
if (principal instanceof OAuth2User) { if (principal instanceof OAuth2User) {
userService.processSSOPostLogin(username, oAuth.getAutoCreateUser()); userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
} }
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;

View File

@@ -3,14 +3,12 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -30,26 +28,15 @@ public class CertificateUtils {
} }
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception { public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PEMParser pemParser = try (PemReader pemReader =
new PEMParser( new PemReader(
new InputStreamReader( new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) { privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
Object object = pemParser.readObject(); byte[] decodedKey = pemObject.getContent();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); return (RSAPrivateKey)
KeyFactory.getInstance("RSA")
if (object instanceof PEMKeyPair) { .generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
// 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"));
}
} }
} }
} }

View File

@@ -12,7 +12,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService; import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@@ -21,11 +20,11 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class CustomSaml2AuthenticationSuccessHandler public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler { extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService; private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties; private ApplicationProperties applicationProperties;
private UserService userService; private UserService userService;
@@ -35,12 +34,10 @@ public class CustomSaml2AuthenticationSuccessHandler
throws ServletException, IOException { throws ServletException, IOException {
Object principal = authentication.getPrincipal(); Object principal = authentication.getPrincipal();
log.debug("Starting SAML2 authentication success handling");
if (principal instanceof CustomSaml2AuthenticatedPrincipal) { if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName(); String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
log.debug("Authenticated principal found for user: {}", username); // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
SavedRequest savedRequest = SavedRequest savedRequest =
@@ -48,77 +45,46 @@ public class CustomSaml2AuthenticationSuccessHandler
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null; : null;
log.debug(
"Session exists: {}, Saved request exists: {}",
session != null,
savedRequest != null);
if (savedRequest != null if (savedRequest != null
&& !RequestUriUtils.isStaticResource( && !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) { contextPath, savedRequest.getRedirectUrl())) {
log.debug( // Redirect to the original destination
"Valid saved request found, redirecting to original destination: {}",
savedRequest.getRedirectUrl());
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} else { } else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2(); SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
log.debug(
"Processing SAML2 authentication with autoCreateUser: {}",
saml2.getAutoCreateUser());
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
log.debug("User {} is blocked due to too many login attempts", username);
if (session != null) { if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST"); session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
} }
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (userService.usernameExistsIgnoreCase(username)
boolean userExists = userService.usernameExistsIgnoreCase(username); && userService.hasPassword(username)
boolean hasPassword = userExists && userService.hasPassword(username); && !userService.isAuthenticationTypeByUsername(
boolean isSSOUser = username, AuthenticationType.OAUTH2)
userExists && saml2.getAutoCreateUser()) {
&& userService.isAuthenticationTypeByUsername(
username, AuthenticationType.SSO);
log.debug(
"User status - Exists: {}, Has password: {}, Is SSO user: {}",
userExists,
hasPassword,
isSSOUser);
if (userExists && hasPassword && !isSSOUser && saml2.getAutoCreateUser()) {
log.debug(
"User {} exists with password but is not SSO user, redirecting to logout",
username);
response.sendRedirect( response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true"); contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return; return;
} }
try { try {
if (saml2.getBlockRegistration() && !userExists) { if (saml2.getBlockRegistration()
log.debug("Registration blocked for new user: {}", username); && !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect( response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user"); contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return; return;
} }
log.debug("Processing SSO post-login for user: {}", username); userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
userService.processSSOPostLogin(username, saml2.getAutoCreateUser());
log.debug("Successfully processed authentication for user: {}", username);
response.sendRedirect(contextPath + "/"); response.sendRedirect(contextPath + "/");
return; return;
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.debug(
"Invalid username detected for user: {}, redirecting to logout",
username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true"); response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return; return;
} }
} }
} else { } else {
log.debug("Non-SAML2 principal detected, delegating to parent handler");
super.onAuthenticationSuccess(request, response, authentication); super.onAuthenticationSuccess(request, response, authentication);
} }
} }

View File

@@ -3,6 +3,8 @@ package stirling.software.SPDF.config.security.saml2;
import java.util.*; import java.util.*;
import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.XSBoolean;
import org.opensaml.core.xml.schema.XSString;
import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Assertion;
import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.AttributeStatement;
@@ -28,60 +30,15 @@ public class CustomSaml2ResponseAuthenticationConverter
this.userService = userService; this.userService = userService;
} }
private Map<String, List<Object>> extractAttributes(Assertion assertion) {
Map<String, List<Object>> attributes = new HashMap<>();
for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
String attributeName = attribute.getName();
List<Object> values = new ArrayList<>();
for (XMLObject xmlObject : attribute.getAttributeValues()) {
// Get the text content directly
String value = xmlObject.getDOM().getTextContent();
if (value != null && !value.trim().isEmpty()) {
values.add(value);
}
}
if (!values.isEmpty()) {
// Store with both full URI and last part of the URI
attributes.put(attributeName, values);
String shortName = attributeName.substring(attributeName.lastIndexOf('/') + 1);
attributes.put(shortName, values);
}
}
}
return attributes;
}
@Override @Override
public Saml2Authentication convert(ResponseToken responseToken) { public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0); Assertion assertion = responseToken.getResponse().getAssertions().get(0);
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Debug log with actual values // Extract the NameID
log.debug("Extracted SAML Attributes: " + attributes); String nameId = assertion.getSubject().getNameID().getValue();
// Try to get username/identifier in order of preference Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
String userIdentifier = null;
if (hasAttribute(attributes, "username")) {
userIdentifier = getFirstAttributeValue(attributes, "username");
} else if (hasAttribute(attributes, "emailaddress")) {
userIdentifier = getFirstAttributeValue(attributes, "emailaddress");
} else if (hasAttribute(attributes, "name")) {
userIdentifier = getFirstAttributeValue(attributes, "name");
} else if (hasAttribute(attributes, "upn")) {
userIdentifier = getFirstAttributeValue(attributes, "upn");
} else if (hasAttribute(attributes, "uid")) {
userIdentifier = getFirstAttributeValue(attributes, "uid");
} else {
userIdentifier = assertion.getSubject().getNameID().getValue();
}
// Rest of your existing code...
Optional<User> userOpt = userService.findByUsernameIgnoreCase(userIdentifier);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER"); SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
@@ -91,27 +48,39 @@ public class CustomSaml2ResponseAuthenticationConverter
} }
} }
// Extract the SessionIndexes
List<String> sessionIndexes = new ArrayList<>(); List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) { for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
sessionIndexes.add(authnStatement.getSessionIndex()); sessionIndexes.add(authnStatement.getSessionIndex());
} }
CustomSaml2AuthenticatedPrincipal principal = // Extract the Attributes
new CustomSaml2AuthenticatedPrincipal( Map<String, List<Object>> attributes = extractAttributes(assertion);
userIdentifier, attributes, userIdentifier, sessionIndexes);
// Create the custom principal
CustomSaml2AuthenticatedPrincipal principal =
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
// Create the Saml2Authentication
return new Saml2Authentication( return new Saml2Authentication(
principal, principal,
responseToken.getToken().getSaml2Response(), responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority)); Collections.singletonList(simpleGrantedAuthority));
} }
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) { private Map<String, List<Object>> extractAttributes(Assertion assertion) {
return attributes.containsKey(name) && !attributes.get(name).isEmpty(); Map<String, List<Object>> attributes = new HashMap<>();
} for (AttributeStatement attributeStatement : assertion.getAttributeStatements()) {
for (Attribute attribute : attributeStatement.getAttributes()) {
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) { String attributeName = attribute.getName();
List<Object> values = attributes.get(name); List<Object> values = new ArrayList<>();
return values != null && !values.isEmpty() ? values.get(0).toString() : null; for (XMLObject xmlObject : attribute.getAttributeValues()) {
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
values.add(((XSString) xmlObject).getValue());
}
attributes.put(attributeName, values);
}
}
return attributes;
} }
} }

View File

@@ -52,115 +52,84 @@ public class SplitPDFController {
"This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException { throws IOException {
MultipartFile file = request.getFileInput();
String pages = request.getPageNumbers();
// open the pdf document
PDDocument document = null; PDDocument document = Loader.loadPDF(file.getBytes());
Path zipFile = null; // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
logger.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
int previousPageNumber = 0;
try { for (int splitPoint : pageNumbers) {
try (PDDocument splitDocument =
MultipartFile file = request.getFileInput(); pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
String pages = request.getPageNumbers(); for (int i = previousPageNumber; i <= splitPoint; i++) {
// open the pdf document PDPage page = document.getPage(i);
splitDocument.addPage(page);
document = Loader.loadPDF(file.getBytes()); logger.info("Adding page {} to split document", i);
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);
if (!pageNumbers.contains(totalPages - 1)) {
// Create a mutable ArrayList so we can add to it
pageNumbers = new ArrayList<>(pageNumbers);
pageNumbers.add(totalPages - 1);
}
logger.info(
"Splitting PDF into pages: {}",
pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(",")));
// split the document
splitDocumentsBoas = new ArrayList<>();
int previousPageNumber = 0;
for (int splitPoint : pageNumbers) {
try (PDDocument splitDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(document)) {
for (int i = previousPageNumber; i <= splitPoint; i++) {
PDPage page = document.getPage(i);
splitDocument.addPage(page);
logger.info("Adding page {} to split document", i);
}
previousPageNumber = splitPoint + 1;
// Transfer metadata to split pdf
// PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
splitDocumentsBoas.add(baos);
} catch (Exception e) {
logger.error("Failed splitting documents and saving them", e);
throw e;
} }
} previousPageNumber = splitPoint + 1;
// closing the original document // Transfer metadata to split pdf
document.close(); // PdfMetadataService.setMetadataToPdf(splitDocument, metadata);
zipFile = Files.createTempFile("split_documents", ".zip"); ByteArrayOutputStream baos = new ByteArrayOutputStream();
splitDocument.save(baos);
String filename = splitDocumentsBoas.add(baos);
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
// loop through the split documents and write them to the zip file
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
logger.info("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) { } catch (Exception e) {
logger.error("Failed writing to zip", e); logger.error("Failed splitting documents and saving them", e);
throw e; throw e;
} }
logger.info(
"Successfully created zip file with split documents: {}", zipFile.toString());
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
// return the Resource in the response
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
try {
// Close the main document
if (document != null) {
document.close();
}
// Close all ByteArrayOutputStreams
for (ByteArrayOutputStream baos : splitDocumentsBoas) {
if (baos != null) {
baos.close();
}
}
// Delete temporary zip file
if (zipFile != null) {
Files.deleteIfExists(zipFile);
}
} catch (Exception e) {
logger.error("Error while cleaning up resources", e);
}
} }
// closing the original document
document.close();
Path zipFile = Files.createTempFile("split_documents", ".zip");
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
// loop through the split documents and write them to the zip file
for (int i = 0; i < splitDocumentsBoas.size(); i++) {
String fileName = filename + "_" + (i + 1) + ".pdf";
ByteArrayOutputStream baos = splitDocumentsBoas.get(i);
byte[] pdf = baos.toByteArray();
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(fileName);
zipOut.putNextEntry(pdfEntry);
zipOut.write(pdf);
zipOut.closeEntry();
logger.info("Wrote split document {} to zip file", fileName);
}
} catch (Exception e) {
logger.error("Failed writing to zip", e);
throw e;
}
logger.info("Successfully created zip file with split documents: {}", zipFile.toString());
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
// return the Resource in the response
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View File

@@ -59,86 +59,70 @@ public class SplitPdfByChaptersController {
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfByChaptersRequest request)
throws Exception { throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument sourceDocument = null; boolean includeMetadata = request.getIncludeMetadata();
Path zipFile = null; Integer bookmarkLevel =
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
try { if (bookmarkLevel < 0) {
boolean includeMetadata = request.getIncludeMetadata(); return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
Integer bookmarkLevel =
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
if (bookmarkLevel < 0) {
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
}
sourceDocument = Loader.loadPDF(file.getBytes());
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
logger.warn("No outline found for {}", file.getOriginalFilename());
return ResponseEntity.badRequest().body("No outline found".getBytes());
}
List<Bookmark> bookmarks = new ArrayList<>();
try {
bookmarks =
extractOutlineItems(
sourceDocument,
outline.getFirstChild(),
bookmarks,
outline.getFirstChild().getNextSibling(),
0,
bookmarkLevel);
// to handle last page edge case
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
} catch (Exception e) {
logger.error("Unable to extract outline items", e);
return ResponseEntity.internalServerError()
.body("Unable to extract outline items".getBytes());
}
boolean allowDuplicates = request.getAllowDuplicates();
if (!allowDuplicates) {
/*
duplicates are generated when multiple bookmarks correspond to the same page,
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
the bookmarks that correspond to the same page, and treat them as a single bookmark
*/
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
}
for (Bookmark bookmark : bookmarks) {
logger.info(
"{}::::{} to {}",
bookmark.getTitle(),
bookmark.getStartPage(),
bookmark.getEndPage());
}
List<ByteArrayOutputStream> splitDocumentsBoas =
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
zipFile = createZipFile(bookmarks, splitDocumentsBoas);
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} finally {
try {
if (sourceDocument != null) {
sourceDocument.close();
}
if (zipFile != null) {
Files.deleteIfExists(zipFile);
}
} catch (Exception e) {
logger.error("Error while cleaning up resources", e);
}
} }
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
logger.warn("No outline found for {}", file.getOriginalFilename());
return ResponseEntity.badRequest().body("No outline found".getBytes());
}
List<Bookmark> bookmarks = new ArrayList<>();
try {
bookmarks =
extractOutlineItems(
sourceDocument,
outline.getFirstChild(),
bookmarks,
outline.getFirstChild().getNextSibling(),
0,
bookmarkLevel);
// to handle last page edge case
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
Bookmark lastBookmark = bookmarks.get(bookmarks.size() - 1);
} catch (Exception e) {
logger.error("Unable to extract outline items", e);
return ResponseEntity.internalServerError()
.body("Unable to extract outline items".getBytes());
}
boolean allowDuplicates = request.getAllowDuplicates();
if (!allowDuplicates) {
/*
duplicates are generated when multiple bookmarks correspond to the same page,
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
the bookmarks that correspond to the same page, and treat them as a single bookmark
*/
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
}
for (Bookmark bookmark : bookmarks) {
logger.info(
"{}::::{} to {}",
bookmark.getTitle(),
bookmark.getStartPage(),
bookmark.getEndPage());
}
List<ByteArrayOutputStream> splitDocumentsBoas =
getSplitDocumentsBoas(sourceDocument, bookmarks, includeMetadata);
Path zipFile = createZipFile(bookmarks, splitDocumentsBoas);
byte[] data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
sourceDocument.close();
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) { private List<Bookmark> mergeBookmarksThatCorrespondToSamePage(List<Bookmark> bookmarks) {

View File

@@ -105,13 +105,15 @@ public class SplitPdfBySectionsController {
if (sectionNum == horiz * verti) pageNum++; if (sectionNum == horiz * verti) pageNum++;
} }
data = Files.readAllBytes(zipFile); } catch (Exception e) {
return WebResponseUtils.bytesToWebResponse( logger.error("exception", e);
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} finally { } finally {
data = Files.readAllBytes(zipFile);
Files.deleteIfExists(zipFile); Files.deleteIfExists(zipFile);
} }
return WebResponseUtils.bytesToWebResponse(
data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM);
} }
public List<PDDocument> splitPdfPages( public List<PDDocument> splitPdfPages(

View File

@@ -244,8 +244,8 @@ public class UserController {
return new RedirectView("/addUsers?messageType=invalidRole", true); return new RedirectView("/addUsers?messageType=invalidRole", true);
} }
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { if (authType.equalsIgnoreCase(AuthenticationType.OAUTH2.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role); userService.saveUser(username, AuthenticationType.OAUTH2, role);
} else { } else {
if (password.isBlank()) { if (password.isBlank()) {
return new RedirectView("/addUsers?messageType=invalidPassword", true); return new RedirectView("/addUsers?messageType=invalidPassword", true);

View File

@@ -65,137 +65,112 @@ public class ConvertImgPDFController {
String colorType = request.getColorType(); String colorType = request.getColorType();
String dpi = request.getDpi(); String dpi = request.getDpi();
Path tempFile = null; byte[] pdfBytes = file.getBytes();
Path tempOutputDir = null; ImageType colorTypeResult = ImageType.RGB;
Path tempPdfPath = null; if ("greyscale".equals(colorType)) {
colorTypeResult = ImageType.GRAY;
} else if ("blackwhite".equals(colorType)) {
colorTypeResult = ImageType.BINARY;
}
// returns bytes for image
boolean singleImage = "single".equals(singleOrMultiple);
byte[] result = null; byte[] result = null;
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
try { result =
byte[] pdfBytes = file.getBytes(); PdfUtils.convertFromPdf(
ImageType colorTypeResult = ImageType.RGB; pdfBytes,
if ("greyscale".equals(colorType)) { "webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
colorTypeResult = ImageType.GRAY; colorTypeResult,
} else if ("blackwhite".equals(colorType)) { singleImage,
colorTypeResult = ImageType.BINARY; Integer.valueOf(dpi),
} filename);
// returns bytes for image if (result == null || result.length == 0) {
boolean singleImage = "single".equals(singleOrMultiple); logger.error("resultant bytes for {} is null, error converting ", filename);
String filename = }
Filenames.toSimpleFileName(file.getOriginalFilename()) if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
.replaceFirst("[.][^.]+$", ""); throw new IOException("Python is not installed. Required for WebP conversion.");
} else if ("webp".equalsIgnoreCase(imageFormat)
result = && CheckProgramInstall.isPythonAvailable()) {
PdfUtils.convertFromPdf( // Write the output stream to a temp file
pdfBytes, Path tempFile = Files.createTempFile("temp_png", ".png");
"webp".equalsIgnoreCase(imageFormat) try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
? "png" fos.write(result);
: imageFormat.toUpperCase(), fos.flush();
colorTypeResult,
singleImage,
Integer.valueOf(dpi),
filename);
if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename);
}
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
} else if ("webp".equalsIgnoreCase(imageFormat)
&& CheckProgramInstall.isPythonAvailable()) {
// Write the output stream to a temp file
tempFile = Files.createTempFile("temp_png", ".png");
try (FileOutputStream fos = new FileOutputStream(tempFile.toFile())) {
fos.write(result);
fos.flush();
}
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
List<String> command = new ArrayList<>();
command.add(pythonVersion);
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
// Create a temporary directory for the output WebP files
tempOutputDir = Files.createTempDirectory("webp_output");
if (singleImage) {
// Run the Python script to convert PNG to WebP
command.add(tempFile.toString());
command.add(tempOutputDir.toString());
command.add("--single");
} else {
// Save the uploaded PDF to a temporary file
tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
file.transferTo(tempPdfPath.toFile());
// Run the Python script to convert PDF to WebP
command.add(tempPdfPath.toString());
command.add(tempOutputDir.toString());
}
command.add("--dpi");
command.add(dpi);
ProcessExecutorResult resultProcess =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Find all WebP files in the output directory
List<Path> webpFiles =
Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp"))
.collect(Collectors.toList());
if (webpFiles.isEmpty()) {
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
throw new IOException(
"No WebP files were created. " + resultProcess.getMessages());
}
byte[] bodyBytes = new byte[0];
if (webpFiles.size() == 1) {
// Return the single WebP file directly
Path webpFilePath = webpFiles.get(0);
bodyBytes = Files.readAllBytes(webpFilePath);
} else {
// Create a ZIP file containing all WebP images
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
}
bodyBytes = zipOutputStream.toByteArray();
}
// Clean up the temporary files
Files.deleteIfExists(tempFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
result = bodyBytes;
} }
String pythonVersion = CheckProgramInstall.getAvailablePythonCommand();
List<String> command = new ArrayList<>();
command.add(pythonVersion);
command.add("./scripts/png_to_webp.py"); // Python script to handle the conversion
// Create a temporary directory for the output WebP files
Path tempOutputDir = Files.createTempDirectory("webp_output");
if (singleImage) { if (singleImage) {
String docName = filename + "." + imageFormat; // Run the Python script to convert PNG to WebP
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat)); command.add(tempFile.toString());
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType); command.add(tempOutputDir.toString());
command.add("--single");
} else { } else {
String zipFilename = filename + "_convertedToImages.zip"; // Save the uploaded PDF to a temporary file
return WebResponseUtils.bytesToWebResponse( Path tempPdfPath = Files.createTempFile("temp_pdf", ".pdf");
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM); file.transferTo(tempPdfPath.toFile());
// Run the Python script to convert PDF to WebP
command.add(tempPdfPath.toString());
command.add(tempOutputDir.toString());
}
command.add("--dpi");
command.add(dpi);
ProcessExecutorResult resultProcess =
ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV)
.runCommandWithOutputHandling(command);
// Find all WebP files in the output directory
List<Path> webpFiles =
Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp"))
.collect(Collectors.toList());
if (webpFiles.isEmpty()) {
logger.error("No WebP files were created in: {}", tempOutputDir.toString());
throw new IOException("No WebP files were created. " + resultProcess.getMessages());
} }
} finally { byte[] bodyBytes = new byte[0];
try {
// Clean up temporary files if (webpFiles.size() == 1) {
if (tempFile != null) { // Return the single WebP file directly
Files.deleteIfExists(tempFile); Path webpFilePath = webpFiles.get(0);
bodyBytes = Files.readAllBytes(webpFilePath);
} else {
// Create a ZIP file containing all WebP images
ByteArrayOutputStream zipOutputStream = new ByteArrayOutputStream();
try (ZipOutputStream zos = new ZipOutputStream(zipOutputStream)) {
for (Path webpFile : webpFiles) {
zos.putNextEntry(new ZipEntry(webpFile.getFileName().toString()));
Files.copy(webpFile, zos);
zos.closeEntry();
}
} }
if (tempPdfPath != null) { bodyBytes = zipOutputStream.toByteArray();
Files.deleteIfExists(tempPdfPath);
}
if (tempOutputDir != null) {
FileUtils.deleteDirectory(tempOutputDir.toFile());
}
} catch (Exception e) {
logger.error("Error cleaning up temporary files", e);
} }
// Clean up the temporary files
Files.deleteIfExists(tempFile);
if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile());
result = bodyBytes;
}
if (singleImage) {
String docName = filename + "." + imageFormat;
MediaType mediaType = MediaType.parseMediaType(getMediaType(imageFormat));
return WebResponseUtils.bytesToWebResponse(result, docName, mediaType);
} else {
String zipFilename = filename + "_convertedToImages.zip";
return WebResponseUtils.bytesToWebResponse(
result, zipFilename, MediaType.APPLICATION_OCTET_STREAM);
} }
} }

View File

@@ -1,13 +1,12 @@
package stirling.software.SPDF.controller.api.converters; 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -38,90 +37,59 @@ public class ConvertPDFToPDFA {
@Operation( @Operation(
summary = "Convert a PDF to a PDF/A", summary = "Convert a PDF to a PDF/A",
description = description =
"This endpoint converts a PDF file to a PDF/A file 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) public ResponseEntity<byte[]> pdfToPdfA(@ModelAttribute PdfToPdfARequest request)
throws Exception { throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat(); String outputFormat = request.getOutputFormat();
// Validate input file type // Convert MultipartFile to byte[]
if (!"application/pdf".equals(inputFile.getContentType())) { byte[] pdfBytes = inputFile.getBytes();
logger.error("Invalid input file type: {}", inputFile.getContentType());
throw new IllegalArgumentException("Input file must be a PDF"); // 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 // Prepare the output file path
String originalFileName = Filenames.toSimpleFileName(inputFile.getOriginalFilename()); Path tempOutputFile = Files.createTempFile("output_", ".pdf");
if (originalFileName == null || originalFileName.trim().isEmpty()) {
originalFileName = "output.pdf";
}
String baseFileName =
originalFileName.contains(".")
? originalFileName.substring(0, originalFileName.lastIndexOf('.'))
: originalFileName;
Path tempInputFile = null; // Prepare the ghostscript command
Path tempOutputDir = null; List<String> command = new ArrayList<>();
byte[] fileBytes; 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 { try {
// Save uploaded file to temp location byte[] pdfBytesOutput = Files.readAllBytes(tempOutputFile);
tempInputFile = Files.createTempFile("input_", ".pdf"); // Return the optimized PDF as a response
inputFile.transferTo(tempInputFile); String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
// Create temp output directory .replaceFirst("[.][^.]+$", "")
tempOutputDir = Files.createTempDirectory("output_"); + "_PDFA.pdf";
// Determine PDF/A filter based on requested format
String pdfFilter =
"pdfa".equals(outputFormat)
? "writer_pdf_Export:{'SelectPdfVersion':{'Value':'2'}}:writer_pdf_Export"
: "writer_pdf_Export:{'SelectPdfVersion':{'Value':'1'}}:writer_pdf_Export";
// Prepare LibreOffice command
List<String> command =
new ArrayList<>(
Arrays.asList(
"soffice",
"--headless",
"--nologo",
"--convert-to",
"pdf:" + pdfFilter,
"--outdir",
tempOutputDir.toString(),
tempInputFile.toString()));
ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE)
.runCommandWithOutputHandling(command);
if (returnCode.getRc() != 0) {
logger.error("PDF/A conversion failed with return code: {}", returnCode.getRc());
throw new RuntimeException("PDF/A conversion failed");
}
// Get the output file
File[] outputFiles = tempOutputDir.toFile().listFiles();
if (outputFiles == null || outputFiles.length != 1) {
throw new RuntimeException(
"Expected exactly one output file but found "
+ (outputFiles == null ? "none" : outputFiles.length));
}
fileBytes = FileUtils.readFileToByteArray(outputFiles[0]);
String outputFilename = baseFileName + "_PDFA.pdf";
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
fileBytes, outputFilename, MediaType.APPLICATION_PDF); pdfBytesOutput, outputFilename, MediaType.APPLICATION_PDF);
} finally { } finally {
// Clean up temporary files // Clean up the temporary files
if (tempInputFile != null) { Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempInputFile); Files.deleteIfExists(tempOutputFile);
}
if (tempOutputDir != null) {
FileUtils.deleteDirectory(tempOutputDir.toFile());
}
} }
} }
} }

View File

@@ -1,12 +1,12 @@
package stirling.software.SPDF.controller.api.converters; package stirling.software.SPDF.controller.api.converters;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.QuoteMode;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ContentDisposition; import org.springframework.http.ContentDisposition;
@@ -18,23 +18,21 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.opencsv.CSVWriter;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.controller.api.CropController;
import stirling.software.SPDF.controller.api.strippers.PDFTableStripper;
import stirling.software.SPDF.model.api.extract.PDFFilePage; import stirling.software.SPDF.model.api.extract.PDFFilePage;
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
import technology.tabula.ObjectExtractor;
import technology.tabula.Page;
import technology.tabula.Table;
import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
import technology.tabula.writers.Writer;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ExtractCSVController { public class ExtractCSVController {
private static final Logger logger = LoggerFactory.getLogger(ExtractCSVController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data") @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation( @Operation(
@@ -42,16 +40,57 @@ public class ExtractCSVController {
description = description =
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception { public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception {
StringWriter writer = new StringWriter();
ArrayList<String> tableData = new ArrayList<>();
int columnsCount = 0;
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) { try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
CSVFormat format = final double res = 72; // PDF units are at 72 DPI
CSVFormat.EXCEL.builder().setEscape('"').setQuoteMode(QuoteMode.ALL).build(); PDFTableStripper stripper = new PDFTableStripper();
Writer csvWriter = new FlexibleCSVWriter(format); PDPage pdPage = document.getPage(form.getPageId() - 1);
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm(); stripper.extractTable(pdPage);
try (ObjectExtractor extractor = new ObjectExtractor(document)) { columnsCount = stripper.getColumns();
Page page = extractor.extract(form.getPageId()); for (int c = 0; c < columnsCount; ++c) {
List<Table> tables = sea.extract(page); for (int r = 0; r < stripper.getRows(); ++r) {
csvWriter.write(writer, tables); 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()); 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;
}
} }

View File

@@ -10,6 +10,7 @@ import java.util.List;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader; import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
@@ -52,54 +53,6 @@ public class CompressController {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor) throws Exception {
byte[] fileBytes = Files.readAllBytes(pdfFile);
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
double scaleFactor = initialScaleFactor;
for (PDPage page : doc.getPages()) {
PDResources res = page.getResources();
if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
if (xobj instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xobj;
BufferedImage bufferedImage = image.getImage();
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
if (newWidth == 0 || newHeight == 0) {
continue;
}
Image scaledImage =
bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
BufferedImage scaledBufferedImage =
new BufferedImage(
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null);
ByteArrayOutputStream compressedImageStream =
new ByteArrayOutputStream();
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close();
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc, imageBytes, image.getCOSObject().toString());
res.put(name, compressedImage);
}
}
}
}
doc.save(pdfFile.toString());
}
}
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@Operation( @Operation(
summary = "Optimize PDF file", summary = "Optimize PDF file",
@@ -122,92 +75,209 @@ public class CompressController {
autoMode = true; autoMode = true;
} }
// Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
long inputFileSize = Files.size(tempInputFile); long inputFileSize = Files.size(tempInputFile);
// Prepare the output file path
Path tempOutputFile = null; Path tempOutputFile = null;
byte[] pdfBytes; byte[] pdfBytes;
try { try {
tempOutputFile = Files.createTempFile("output_", ".pdf"); tempOutputFile = Files.createTempFile("output_", ".pdf");
// Determine initial optimization level based on expected size reduction, only if in
// autoMode
if (autoMode) { if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
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; boolean sizeMet = false;
while (!sizeMet && optimizeLevel <= 9) { while (!sizeMet && optimizeLevel <= 4) {
// Prepare the Ghostscript command
// 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
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("qpdf"); command.add("gs");
if (request.getNormalize()) { command.add("-sDEVICE=pdfwrite");
command.add("--normalize-content=y"); 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("-dNOPAUSE");
} command.add("-dQUIET");
command.add("--optimize-images"); command.add("-dBATCH");
command.add("--recompress-flate"); command.add("-sOutputFile=" + tempOutputFile.toString());
command.add("--compression-level=" + optimizeLevel);
command.add("--compress-streams=y");
command.add("--object-streams=generate");
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = null; ProcessExecutorResult returnCode =
try { ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
returnCode = .runCommandWithOutputHandling(command);
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
} catch (Exception e) {
if (returnCode != null && returnCode.getRc() != 3) {
throw e;
}
}
// Check if file size is within expected size or not auto mode // Check if file size is within expected size or not auto mode so instantly finish
long outputFileSize = Files.size(tempOutputFile); long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize <= expectedOutputSize || !autoMode) { if (outputFileSize <= expectedOutputSize || !autoMode) {
sizeMet = true; sizeMet = true;
} else { } else {
optimizeLevel = // Increase optimization level for next iteration
incrementOptimizeLevel( optimizeLevel++;
optimizeLevel, outputFileSize, expectedOutputSize); if (autoMode && optimizeLevel > 4) {
if (autoMode && optimizeLevel > 9) { logger.info("Skipping level 5 due to bad results in auto mode");
logger.info("Maximum compression level reached in auto mode");
sizeMet = true; sizeMet = true;
} else {
logger.info(
"Increasing ghostscript optimisation level to " + optimizeLevel);
} }
} }
} }
if (expectedOutputSize != null && autoMode) {
long outputFileSize = Files.size(tempOutputFile);
byte[] fileBytes = Files.readAllBytes(tempOutputFile);
if (outputFileSize > expectedOutputSize) {
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
long previousFileSize = 0;
double scaleFactorConst = 0.9f;
double scaleFactor = 0.9f;
while (true) {
for (PDPage page : doc.getPages()) {
PDResources res = page.getResources();
if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
if (xobj != null && xobj instanceof PDImageXObject) {
PDImageXObject image = (PDImageXObject) xobj;
// Get the image in BufferedImage format
BufferedImage bufferedImage = image.getImage();
// Calculate the new dimensions
int newWidth =
(int)
(bufferedImage.getWidth()
* scaleFactorConst);
int newHeight =
(int)
(bufferedImage.getHeight()
* scaleFactorConst);
// If the new dimensions are zero, skip this iteration
if (newWidth == 0 || newHeight == 0) {
continue;
}
// Otherwise, proceed with the scaling
Image scaledImage =
bufferedImage.getScaledInstance(
newWidth,
newHeight,
Image.SCALE_SMOOTH);
// Convert the scaled image back to a BufferedImage
BufferedImage scaledBufferedImage =
new BufferedImage(
newWidth,
newHeight,
BufferedImage.TYPE_INT_RGB);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
// Compress the scaled image
ByteArrayOutputStream compressedImageStream =
new ByteArrayOutputStream();
ImageIO.write(
scaledBufferedImage,
"jpeg",
compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close();
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc,
imageBytes,
image.getCOSObject().toString());
// Replace the image in the resources with the
// compressed
// version
res.put(name, compressedImage);
}
}
}
}
// save the document to tempOutputFile again
doc.save(tempOutputFile.toString());
long currentSize = Files.size(tempOutputFile);
// Check if the overall PDF size is still larger than expectedOutputSize
if (currentSize > expectedOutputSize) {
// Log the current file size and scaleFactor
logger.info(
"Current file size: "
+ FileUtils.byteCountToDisplaySize(currentSize));
logger.info("Current scale factor: " + scaleFactor);
// The file is still too large, reduce scaleFactor and try again
scaleFactor *= 0.9f; // reduce scaleFactor by 10%
// Avoid scaleFactor being too small, causing the image to shrink to
// 0
if (scaleFactor < 0.2f || previousFileSize == currentSize) {
throw new RuntimeException(
"Could not reach the desired size without excessively degrading image quality, lowest size recommended is "
+ FileUtils.byteCountToDisplaySize(currentSize)
+ ", "
+ currentSize
+ " bytes");
}
previousFileSize = currentSize;
} else {
// The file is small enough, break the loop
break;
}
}
}
}
}
// Read the optimized PDF file // Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile); pdfBytes = Files.readAllBytes(tempOutputFile);
Path finalFile = tempOutputFile; Path finalFile = tempOutputFile;
// Check if optimized file is larger than the original // Check if optimized file is larger than the original
if (pdfBytes.length > inputFileSize) { if (pdfBytes.length > inputFileSize) {
// Log the occurrence
logger.warn( logger.warn(
"Optimized file is larger than the original. Returning the original file instead."); "Optimized file is larger than the original. Returning the original file instead.");
// Read the original file again
finalFile = tempInputFile; finalFile = tempInputFile;
} }
// Return the optimized PDF as a response
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
@@ -216,31 +286,10 @@ public class CompressController {
pdfDocumentFactory.load(finalFile.toFile()), outputFilename); pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
} finally { } finally {
// Clean up the temporary files
// deleted by multipart file handler deu to transferTo?
// Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile); Files.deleteIfExists(tempOutputFile);
} }
} }
private int determineOptimizeLevel(double sizeReductionRatio) {
if (sizeReductionRatio > 0.9) return 1;
if (sizeReductionRatio > 0.8) return 2;
if (sizeReductionRatio > 0.7) return 3;
if (sizeReductionRatio > 0.6) return 4;
if (sizeReductionRatio > 0.5) return 5;
if (sizeReductionRatio > 0.4) return 6;
if (sizeReductionRatio > 0.3) return 7;
if (sizeReductionRatio > 0.2) return 8;
return 9;
}
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize;
logger.info("Current compression ratio: {}", String.format("%.2f", currentRatio));
if (currentRatio > 2.0) {
return Math.min(9, currentLevel + 3);
} else if (currentRatio > 1.5) {
return Math.min(9, currentLevel + 2);
}
return Math.min(9, currentLevel + 1);
}
} }

View File

@@ -58,7 +58,7 @@ public class FakeScanControllerWIP {
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = description =
"This endpoint repairs a given PDF file by running 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 { public ResponseEntity<byte[]> fakeScan(@ModelAttribute PDFFile request) throws IOException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();

View File

@@ -14,8 +14,6 @@ import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -28,7 +26,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@@ -47,11 +44,6 @@ public class MetadataController {
return entry; return entry;
} }
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
}
@PostMapping(consumes = "multipart/form-data", value = "/update-metadata") @PostMapping(consumes = "multipart/form-data", value = "/update-metadata")
@Operation( @Operation(
summary = "Update metadata of a PDF file", summary = "Update metadata of a PDF file",

View File

@@ -1,29 +1,19 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -33,31 +23,24 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.BoundedLineReader;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
import stirling.software.SPDF.service.CustomPDDocumentFactory; import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/misc") @RequestMapping("/api/v1/misc")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
@Slf4j
public class OCRController { public class OCRController {
@Autowired 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() { public List<String> getAvailableTesseractLanguages() {
String tessdataDir = applicationProperties.getSystem().getTessdataDir(); String tessdataDir = applicationProperties.getSystem().getTessdataDir();
File[] files = new File(tessdataDir).listFiles(); File[] files = new File(tessdataDir).listFiles();
@@ -71,167 +54,196 @@ public class OCRController {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public OCRController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf")
@Operation(
summary = "Process a PDF file with OCR",
description =
"This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional")
public ResponseEntity<byte[]> processPdfWithOCR( public ResponseEntity<byte[]> processPdfWithOCR(
@ModelAttribute ProcessPdfWithOcrRequest request) @ModelAttribute ProcessPdfWithOcrRequest request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
List<String> 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 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"); if (!"hocr".equals(ocrRenderType) && !"sandwich".equals(ocrRenderType)) {
Path tempInputFile = tempDir.resolve("input.pdf"); throw new IOException("ocrRenderType wrong");
Path tempOutputDir = tempDir.resolve("output"); }
Path tempImagesDir = tempDir.resolve("images");
Path finalOutputFile = tempDir.resolve("final_output.pdf"); // 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;
Files.createDirectories(tempOutputDir);
Files.createDirectories(tempImagesDir);
Process process = null;
try { try {
// Save input file
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
PDFMergerUtility merger = new PDFMergerUtility();
merger.setDestinationFileName(finalOutputFile.toString());
try (PDDocument document = pdfDocumentFactory.load(tempInputFile.toFile())) { // Run OCR Command
PDFRenderer pdfRenderer = new PDFRenderer(document); String languageOption = String.join("+", selectedLanguages);
int pageCount = document.getNumberOfPages();
for (int pageNum = 0; pageNum < pageCount; pageNum++) { List<String> command =
PDPage page = document.getPage(pageNum); new ArrayList<>(
boolean hasText = false; Arrays.asList(
"ocrmypdf",
"--verbose",
"2",
"--output-type",
"pdf",
"--pdf-renderer",
ocrRenderType));
// Check for existing text if (sidecar != null && sidecar) {
try (PDDocument tempDoc = new PDDocument()) { sidecarTextPath = Files.createTempFile("sidecar", ".txt");
tempDoc.addPage(page); command.add("--sidecar");
PDFTextStripper stripper = new PDFTextStripper(); command.add(sidecarTextPath.toString());
hasText = !stripper.getText(tempDoc).trim().isEmpty(); }
}
boolean shouldOcr = if (deskew != null && deskew) {
switch (ocrType) { command.add("--deskew");
case "skip-text" -> !hasText; }
case "force-ocr" -> true; if (clean != null && clean) {
default -> true; 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 = 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 command.addAll(
merger.mergeDocuments(null); Arrays.asList(
"--language",
languageOption,
tempInputFile.toString(),
tempOutputFile.toString()));
// Read the final PDF file // Run CLI command
byte[] pdfContent = Files.readAllBytes(finalOutputFile); 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 = String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_OCR.pdf"; + "_OCR.pdf";
return ResponseEntity.ok() if (sidecar != null && sidecar) {
.header( // Create a zip file containing both the PDF and the text file
"Content-Disposition", String outputZipFilename =
"attachment; filename=\"" + outputFilename + "\"") Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.contentType(MediaType.APPLICATION_PDF) .replaceFirst("[.][^.]+$", "")
.body(pdfContent); + "_OCR.zip";
Path tempZipFile = Files.createTempFile("output_", ".zip");
try (ZipOutputStream zipOut =
new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) {
// Add PDF file to the zip
ZipEntry pdfEntry = new ZipEntry(outputFilename);
zipOut.putNextEntry(pdfEntry);
try (ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes)) {
byte[] buffer = new byte[1024];
int length;
while ((length = pdfInputStream.read(buffer)) != -1) {
zipOut.write(buffer, 0, length);
}
}
zipOut.closeEntry();
// Add text file to the zip
ZipEntry txtEntry = new ZipEntry(outputFilename.replace(".pdf", ".txt"));
zipOut.putNextEntry(txtEntry);
Files.copy(sidecarTextPath, zipOut);
zipOut.closeEntry();
}
byte[] zipBytes = Files.readAllBytes(tempZipFile);
// Clean up the temporary zip file
Files.deleteIfExists(tempZipFile);
Files.deleteIfExists(tempOutputFile);
Files.deleteIfExists(sidecarTextPath);
// Return the zip file containing both the PDF and the text file
return WebResponseUtils.bytesToWebResponse(
zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM);
} else {
// Return the OCR processed PDF as a response
Files.deleteIfExists(tempOutputFile);
return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename);
}
} finally { } finally {
if (process != null) { // Clean up the temporary files
process.destroy(); Files.deleteIfExists(tempOutputFile);
// Comment out as transferTo makes multipart handle cleanup
// Files.deleteIfExists(tempInputFile);
if (sidecarTextPath != null) {
Files.deleteIfExists(sidecarTextPath);
} }
// Clean up temporary files
deleteDirectory(tempDir);
}
}
private void addFileToZip(File file, String filename, ZipOutputStream zipOut)
throws IOException {
if (!file.exists()) {
log.warn("File {} does not exist, skipping", file);
return;
}
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry zipEntry = new ZipEntry(filename);
zipOut.putNextEntry(zipEntry);
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) >= 0) {
zipOut.write(buffer, 0, length);
}
zipOut.closeEntry();
}
}
private void deleteDirectory(Path directory) {
try {
Files.walk(directory)
.sorted(Comparator.reverseOrder())
.forEach(
path -> {
try {
Files.delete(path);
} catch (IOException e) {
log.error("Error deleting {}: {}", path, e.getMessage());
}
});
} catch (IOException e) {
log.error("Error walking directory {}: {}", directory, e.getMessage());
} }
} }
} }

View File

@@ -44,29 +44,30 @@ public class RepairController {
@Operation( @Operation(
summary = "Repair a PDF file", summary = "Repair a PDF file",
description = description =
"This endpoint repairs a given PDF file by running 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) public ResponseEntity<byte[]> repairPdf(@ModelAttribute PDFFile request)
throws IOException, InterruptedException { throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
byte[] pdfBytes = null; byte[] pdfBytes = null;
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
try { try {
List<String> command = new ArrayList<>(); List<String> command = new ArrayList<>();
command.add("qpdf"); command.add("gs");
command.add("--replace-input"); // Automatically fixes problems it can command.add("-o");
command.add("--qdf"); // Linearizes and normalizes PDF structure command.add(tempOutputFile.toString());
command.add("--object-streams=disable"); // Can help with some corruptions command.add("-sDEVICE=pdfwrite");
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT)
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile()); pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile());
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = String outputFilename =
@@ -77,6 +78,7 @@ public class RepairController {
} finally { } finally {
// Clean up the temporary files // Clean up the temporary files
Files.deleteIfExists(tempInputFile); Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile);
} }
} }
} }

View File

@@ -229,22 +229,10 @@ public class StampController {
calculatePositionY( calculatePositionY(
pageSize, position, calculateTextCapHeight(font, fontSize), margin); pageSize, position, calculateTextCapHeight(font, fontSize), margin);
} }
// Split the stampText into multiple lines
String[] lines = stampText.split("\\\\n");
// Calculate dynamic line height based on font ascent and descent
float ascent = font.getFontDescriptor().getAscent();
float descent = font.getFontDescriptor().getDescent();
float lineHeight = ((ascent - descent) / 1000) * fontSize;
contentStream.beginText(); contentStream.beginText();
for (int i = 0; i < lines.length; i++) { contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y));
String line = lines[i]; contentStream.showText(stampText);
// Set the text matrix for each line with rotation
contentStream.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
contentStream.showText(line);
}
contentStream.endText(); contentStream.endText();
} }

View File

@@ -221,7 +221,7 @@ public class PipelineProcessor {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser(); String apiKey = getApiKeyForUser();
headers.add("X-API-KEY", apiKey); headers.add("X-API-Key", apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA); headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create HttpEntity with the body and headers // Create HttpEntity with the body and headers

View File

@@ -322,14 +322,27 @@ public class GetInfoOnPDF {
PDEncryption pdfEncryption = pdfBoxDoc.getEncryption(); PDEncryption pdfEncryption = pdfBoxDoc.getEncryption();
encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter()); encryption.put("EncryptionAlgorithm", pdfEncryption.getFilter());
encryption.put("KeyLength", pdfEncryption.getLength()); encryption.put("KeyLength", pdfEncryption.getLength());
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
if (ap != null) {
ObjectNode permissionsNode = objectMapper.createObjectNode();
permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument());
permissionsNode.put("CanExtractContent", ap.canExtractContent());
permissionsNode.put(
"CanExtractForAccessibility", ap.canExtractForAccessibility());
permissionsNode.put("CanFillInForm", ap.canFillInForm());
permissionsNode.put("CanModify", ap.canModify());
permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations());
permissionsNode.put("CanPrint", ap.canPrint());
encryption.set(
"Permissions", permissionsNode); // set the node under "Permissions"
}
// Add other encryption-related properties as needed // Add other encryption-related properties as needed
} else { } else {
encryption.put("IsEncrypted", false); encryption.put("IsEncrypted", false);
} }
ObjectNode permissionsNode = objectMapper.createObjectNode();
setNodePermissions(pdfBoxDoc, permissionsNode);
ObjectNode pageInfoParent = objectMapper.createObjectNode(); ObjectNode pageInfoParent = objectMapper.createObjectNode();
for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) {
ObjectNode pageInfo = objectMapper.createObjectNode(); ObjectNode pageInfo = objectMapper.createObjectNode();
@@ -571,7 +584,6 @@ public class GetInfoOnPDF {
jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("DocumentInfo", docInfoNode);
jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Compliancy", compliancy);
jsonOutput.set("Encryption", encryption); jsonOutput.set("Encryption", encryption);
jsonOutput.set("Permissions", permissionsNode); // set the node under "Permissions"
jsonOutput.set("Other", other); jsonOutput.set("Other", other);
jsonOutput.set("PerPageInfo", pageInfoParent); jsonOutput.set("PerPageInfo", pageInfoParent);
@@ -590,24 +602,6 @@ public class GetInfoOnPDF {
return null; return null;
} }
private void setNodePermissions(PDDocument pdfBoxDoc, ObjectNode permissionsNode) {
AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission();
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));
permissionsNode.put("Printing", getPermissionState(ap.canPrint()));
}
private String getPermissionState(boolean state) {
return state ? "Allowed" : "Not Allowed";
}
private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) {
if (outline == null) return; if (outline == null) return;

View File

@@ -39,7 +39,10 @@ public class PasswordController {
} }
@PostMapping(consumes = "multipart/form-data", value = "/remove-password") @PostMapping(consumes = "multipart/form-data", value = "/remove-password")
@Operation(summary = "Remove password from a PDF file", description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO") @Operation(
summary = "Remove password from a PDF file",
description =
"This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request) public ResponseEntity<byte[]> removePassword(@ModelAttribute PDFPasswordRequest request)
throws IOException { throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
@@ -49,12 +52,15 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename()) Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_password_removed.pdf"); + "_password_removed.pdf");
} }
@PostMapping(consumes = "multipart/form-data", value = "/add-password") @PostMapping(consumes = "multipart/form-data", value = "/add-password")
@Operation(summary = "Add password to a PDF file", description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF") @Operation(
summary = "Add password to a PDF file",
description =
"This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF")
public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request) public ResponseEntity<byte[]> addPassword(@ModelAttribute AddPasswordRequest request)
throws IOException { throws IOException {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();
@@ -92,12 +98,12 @@ public class PasswordController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename()) Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_permissions.pdf"); + "_permissions.pdf");
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(fileInput.getOriginalFilename()) Filenames.toSimpleFileName(fileInput.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_passworded.pdf"); + "_passworded.pdf");
} }
} }

View File

@@ -1,180 +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);
}
}

View File

@@ -69,7 +69,6 @@ public class WatermarkController {
float opacity = request.getOpacity(); float opacity = request.getOpacity();
int widthSpacer = request.getWidthSpacer(); int widthSpacer = request.getWidthSpacer();
int heightSpacer = request.getHeightSpacer(); int heightSpacer = request.getHeightSpacer();
String customColor = request.getCustomColor();
boolean convertPdfToImage = request.isConvertPDFToImage(); boolean convertPdfToImage = request.isConvertPDFToImage();
// Load the input PDF // Load the input PDF
@@ -98,8 +97,7 @@ public class WatermarkController {
widthSpacer, widthSpacer,
heightSpacer, heightSpacer,
fontSize, fontSize,
alphabet, alphabet);
customColor);
} else if ("image".equalsIgnoreCase(watermarkType)) { } else if ("image".equalsIgnoreCase(watermarkType)) {
addImageWatermark( addImageWatermark(
contentStream, contentStream,
@@ -138,8 +136,7 @@ public class WatermarkController {
int widthSpacer, int widthSpacer,
int heightSpacer, int heightSpacer,
float fontSize, float fontSize,
String alphabet, String alphabet)
String colorString)
throws IOException { throws IOException {
String resourceDir = ""; String resourceDir = "";
PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA);
@@ -176,18 +173,7 @@ public class WatermarkController {
} }
contentStream.setFont(font, fontSize); contentStream.setFont(font, fontSize);
contentStream.setNonStrokingColor(Color.LIGHT_GRAY);
Color redactColor;
try {
if (!colorString.startsWith("#")) {
colorString = "#" + colorString;
}
redactColor = Color.decode(colorString);
} catch (NumberFormatException e) {
redactColor = Color.LIGHT_GRAY;
}
contentStream.setNonStrokingColor(redactColor);
String[] textLines = watermarkText.split("\\\\n"); String[] textLines = watermarkText.split("\\\\n");
float maxLineWidth = 0; float maxLineWidth = 0;

View File

@@ -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);
}
}
}

View File

@@ -34,9 +34,7 @@ public class DatabaseWebController {
} }
List<FileInfo> backupList = databaseBackupHelper.getBackupList(); List<FileInfo> backupList = databaseBackupHelper.getBackupList();
model.addAttribute("backupFiles", backupList); model.addAttribute("systemUpdate", backupList);
model.addAttribute("databaseVersion", databaseBackupHelper.getH2Version());
return "database"; return "database";
} }

View File

@@ -55,11 +55,6 @@ public class HomeWebController {
return "licenses"; return "licenses";
} }
@GetMapping("/releases")
public String getReleaseNotes(Model model) {
return "releases";
}
@GetMapping("/") @GetMapping("/")
public String home(Model model) { public String home(Model model) {
model.addAttribute("currentPage", "home"); model.addAttribute("currentPage", "home");

View File

@@ -53,13 +53,6 @@ public class SecurityWebController {
return "security/cert-sign"; return "security/cert-sign";
} }
@GetMapping("/validate-signature")
@Hidden
public String certSignVerifyForm(Model model) {
model.addAttribute("currentPage", "validate-signature");
return "security/validate-signature";
}
@GetMapping("/remove-cert-sign") @GetMapping("/remove-cert-sign")
@Hidden @Hidden
public String certUnSignForm(Model model) { public String certUnSignForm(Model model) {

View File

@@ -73,7 +73,6 @@ public class ApplicationProperties {
private int loginAttemptCount; private int loginAttemptCount;
private long loginResetTimeMinutes; private long loginResetTimeMinutes;
private String loginMethod = "all"; private String loginMethod = "all";
private String customGlobalAPIKey;
public Boolean isAltLogin() { public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled(); return saml2.getEnabled() || oauth2.getEnabled();
@@ -286,7 +285,6 @@ public class ApplicationProperties {
public static class AutomaticallyGenerated { public static class AutomaticallyGenerated {
@ToString.Exclude private String key; @ToString.Exclude private String key;
private String UUID; private String UUID;
private String appVersion;
} }
@Data @Data
@@ -322,20 +320,12 @@ public class ApplicationProperties {
public static class SessionLimit { public static class SessionLimit {
private int libreOfficeSessionLimit; private int libreOfficeSessionLimit;
private int pdfToHtmlSessionLimit; private int pdfToHtmlSessionLimit;
private int ocrMyPdfSessionLimit;
private int pythonOpenCvSessionLimit; private int pythonOpenCvSessionLimit;
private int ghostScriptSessionLimit;
private int weasyPrintSessionLimit; private int weasyPrintSessionLimit;
private int installAppSessionLimit; private int installAppSessionLimit;
private int calibreSessionLimit; private int calibreSessionLimit;
private int qpdfSessionLimit;
private int tesseractSessionLimit;
public int getQpdfSessionLimit() {
return qpdfSessionLimit > 0 ? qpdfSessionLimit : 2;
}
public int getTesseractSessionLimit() {
return tesseractSessionLimit > 0 ? tesseractSessionLimit : 1;
}
public int getLibreOfficeSessionLimit() { public int getLibreOfficeSessionLimit() {
return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1; return libreOfficeSessionLimit > 0 ? libreOfficeSessionLimit : 1;
@@ -345,10 +335,18 @@ public class ApplicationProperties {
return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1; return pdfToHtmlSessionLimit > 0 ? pdfToHtmlSessionLimit : 1;
} }
public int getOcrMyPdfSessionLimit() {
return ocrMyPdfSessionLimit > 0 ? ocrMyPdfSessionLimit : 2;
}
public int getPythonOpenCvSessionLimit() { public int getPythonOpenCvSessionLimit() {
return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8; return pythonOpenCvSessionLimit > 0 ? pythonOpenCvSessionLimit : 8;
} }
public int getGhostScriptSessionLimit() {
return ghostScriptSessionLimit > 0 ? ghostScriptSessionLimit : 16;
}
public int getWeasyPrintSessionLimit() { public int getWeasyPrintSessionLimit() {
return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16; return weasyPrintSessionLimit > 0 ? weasyPrintSessionLimit : 16;
} }
@@ -366,20 +364,12 @@ public class ApplicationProperties {
public static class TimeoutMinutes { public static class TimeoutMinutes {
private long libreOfficeTimeoutMinutes; private long libreOfficeTimeoutMinutes;
private long pdfToHtmlTimeoutMinutes; private long pdfToHtmlTimeoutMinutes;
private long ocrMyPdfTimeoutMinutes;
private long pythonOpenCvTimeoutMinutes; private long pythonOpenCvTimeoutMinutes;
private long ghostScriptTimeoutMinutes;
private long weasyPrintTimeoutMinutes; private long weasyPrintTimeoutMinutes;
private long installAppTimeoutMinutes; private long installAppTimeoutMinutes;
private long calibreTimeoutMinutes; private long calibreTimeoutMinutes;
private long tesseractTimeoutMinutes;
private long qpdfTimeoutMinutes;
public long getTesseractTimeoutMinutes() {
return tesseractTimeoutMinutes > 0 ? tesseractTimeoutMinutes : 30;
}
public long getQpdfTimeoutMinutes() {
return qpdfTimeoutMinutes > 0 ? qpdfTimeoutMinutes : 30;
}
public long getLibreOfficeTimeoutMinutes() { public long getLibreOfficeTimeoutMinutes() {
return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30; return libreOfficeTimeoutMinutes > 0 ? libreOfficeTimeoutMinutes : 30;
@@ -389,10 +379,18 @@ public class ApplicationProperties {
return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20; return pdfToHtmlTimeoutMinutes > 0 ? pdfToHtmlTimeoutMinutes : 20;
} }
public long getOcrMyPdfTimeoutMinutes() {
return ocrMyPdfTimeoutMinutes > 0 ? ocrMyPdfTimeoutMinutes : 30;
}
public long getPythonOpenCvTimeoutMinutes() { public long getPythonOpenCvTimeoutMinutes() {
return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30; return pythonOpenCvTimeoutMinutes > 0 ? pythonOpenCvTimeoutMinutes : 30;
} }
public long getGhostScriptTimeoutMinutes() {
return ghostScriptTimeoutMinutes > 0 ? ghostScriptTimeoutMinutes : 30;
}
public long getWeasyPrintTimeoutMinutes() { public long getWeasyPrintTimeoutMinutes() {
return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30; return weasyPrintTimeoutMinutes > 0 ? weasyPrintTimeoutMinutes : 30;
} }

View File

@@ -2,5 +2,5 @@ package stirling.software.SPDF.model;
public enum AuthenticationType { public enum AuthenticationType {
WEB, WEB,
SSO OAUTH2
} }

View File

@@ -18,15 +18,4 @@ public class OptimizePdfRequest extends PDFFile {
@Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.") @Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.")
private String expectedOutputSize; private String expectedOutputSize;
@Schema(
description = "Whether to linearize the PDF for faster web viewing. Default is false.",
defaultValue = "false")
private Boolean linearize = false;
@Schema(
description =
"Whether to normalize the PDF content for better compatibility. Default is true.",
defaultValue = "true")
private Boolean normalize = true;
} }

View File

@@ -15,6 +15,18 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
@Schema(description = "List of languages to use in OCR processing") @Schema(description = "List of languages to use in OCR processing")
private List<String> languages; private List<String> languages;
@Schema(description = "Include OCR text in a sidecar text file if set to true")
private boolean sidecar;
@Schema(description = "Deskew the input file if set to true")
private boolean deskew;
@Schema(description = "Clean the input file if set to true")
private boolean clean;
@Schema(description = "Clean the final output if set to true")
private boolean cleanFinal;
@Schema( @Schema(
description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'",
allowableValues = {"skip-text", "force-ocr", "Normal"}) allowableValues = {"skip-text", "force-ocr", "Normal"})
@@ -25,4 +37,7 @@ public class ProcessPdfWithOcrRequest extends PDFFile {
allowableValues = {"hocr", "sandwich"}, allowableValues = {"hocr", "sandwich"},
defaultValue = "hocr") defaultValue = "hocr")
private String ocrRenderType = "hocr"; private String ocrRenderType = "hocr";
@Schema(description = "Remove images from the output PDF if set to true")
private boolean removeImagesAfter;
} }

View File

@@ -45,9 +45,6 @@ public class AddWatermarkRequest extends PDFFile {
@Schema(description = "The height spacer between watermark elements", example = "50") @Schema(description = "The height spacer between watermark elements", example = "50")
private int heightSpacer; private int heightSpacer;
@Schema(description = "The color for watermark", defaultValue = "#d3d3d3")
private String customColor = "#d3d3d3";
@Schema(description = "Convert the redacted PDF to an image", defaultValue = "false") @Schema(description = "Convert the redacted PDF to an image", defaultValue = "false")
private boolean convertPDFToImage; private boolean convertPDFToImage;
} }

View File

@@ -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;
}

View File

@@ -1,30 +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
}

View File

@@ -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);
}
}

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.repository; package stirling.software.SPDF.repository;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
@@ -20,6 +19,4 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username); Optional<User> findByUsername(String username);
Optional<User> findByApiKey(String apiKey); Optional<User> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
} }

View File

@@ -1,158 +0,0 @@
package stirling.software.SPDF.service;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.CertPath;
import java.security.cert.CertPathValidator;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.PKIXParameters;
import java.security.cert.TrustAnchor;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.stereotype.Service;
import io.github.pixee.security.BoundedLineReader;
import jakarta.annotation.PostConstruct;
@Service
public class CertificateValidationService {
private KeyStore trustStore;
@PostConstruct
private void initializeTrustStore() throws Exception {
trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
loadMozillaCertificates();
}
private void loadMozillaCertificates() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/certdata.txt")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder certData = new StringBuilder();
boolean inCert = false;
int certCount = 0;
while ((line = BoundedLineReader.readLine(reader, 5_000_000)) != null) {
if (line.startsWith("CKA_VALUE MULTILINE_OCTAL")) {
inCert = true;
certData = new StringBuilder();
continue;
}
if (inCert) {
if ("END".equals(line)) {
inCert = false;
byte[] certBytes = parseOctalData(certData.toString());
if (certBytes != null) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert =
(X509Certificate)
cf.generateCertificate(
new ByteArrayInputStream(certBytes));
trustStore.setCertificateEntry("mozilla-cert-" + certCount++, cert);
}
} else {
certData.append(line).append("\n");
}
}
}
}
}
private byte[] parseOctalData(String data) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String[] tokens = data.split("\\\\");
for (String token : tokens) {
token = token.trim();
if (!token.isEmpty()) {
baos.write(Integer.parseInt(token, 8));
}
}
return baos.toByteArray();
} catch (Exception e) {
return null;
}
}
public boolean validateCertificateChain(X509Certificate cert) {
try {
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
List<X509Certificate> certList = Arrays.asList(cert);
CertPath certPath = cf.generateCertPath(certList);
Set<TrustAnchor> anchors = new HashSet<>();
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate) {
anchors.add(new TrustAnchor((X509Certificate) trustCert, null));
}
}
PKIXParameters params = new PKIXParameters(anchors);
params.setRevocationEnabled(false);
validator.validate(certPath, params);
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustStore(X509Certificate cert) {
try {
Enumeration<String> aliases = trustStore.aliases();
while (aliases.hasMoreElements()) {
Object trustCert = trustStore.getCertificate(aliases.nextElement());
if (trustCert instanceof X509Certificate && cert.equals(trustCert)) {
return true;
}
}
return false;
} catch (KeyStoreException e) {
return false;
}
}
public boolean isRevoked(X509Certificate cert) {
try {
cert.checkValidity();
return false;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
return true;
}
}
public boolean validateCertificateChainWithCustomCert(
X509Certificate cert, X509Certificate customCert) {
try {
cert.verify(customCert.getPublicKey());
return true;
} catch (Exception e) {
return false;
}
}
public boolean validateTrustWithCustomCert(X509Certificate cert, X509Certificate customCert) {
try {
// Compare the issuer of the signature certificate with the custom certificate
return cert.getIssuerX500Principal().equals(customCert.getSubjectX500Principal());
} catch (Exception e) {
return false;
}
}
}

View File

@@ -24,7 +24,7 @@ public class MetricsAggregatorService {
this.postHogService = postHogService; this.postHogService = postHogService;
} }
@Scheduled(fixedRate = 7200000) // Run every 2 hours @Scheduled(fixedRate = 900000) // Run every 15 minutes
public void aggregateAndSendMetrics() { public void aggregateAndSendMetrics() {
Map<String, Object> metrics = new HashMap<>(); Map<String, Object> metrics = new HashMap<>();
Search.in(meterRegistry) Search.in(meterRegistry)
@@ -39,17 +39,12 @@ public class MetricsAggregatorService {
if (method == null || uri == null) { if (method == null || uri == null) {
return; return;
} }
if (!method.equals("GET") && !method.equals("POST")) {
return;
}
// Skip URIs that are 2 characters or shorter
if (uri.length() <= 2) {
return;
}
String key = String key = String.format(
String.format( "http_requests_%s_%s",
"http_requests_%s_%s", method, uri.replace("/", "_")); method,
uri.replace("/", "_")
);
double currentCount = counter.count(); double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0); double lastCount = lastSentMetrics.getOrDefault(key, 0.0);

View File

@@ -17,18 +17,15 @@ public class PdfMetadataService {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final String stirlingPDFLabel; private final String stirlingPDFLabel;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final boolean runningEE;
@Autowired @Autowired
public PdfMetadataService( public PdfMetadataService(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Qualifier("StirlingPDFLabel") String stirlingPDFLabel, @Qualifier("StirlingPDFLabel") String stirlingPDFLabel,
@Qualifier("runningEE") boolean runningEE,
@Autowired(required = false) UserServiceInterface userService) { @Autowired(required = false) UserServiceInterface userService) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.stirlingPDFLabel = stirlingPDFLabel; this.stirlingPDFLabel = stirlingPDFLabel;
this.userService = userService; this.userService = userService;
this.runningEE = runningEE;
} }
public PdfMetadata extractMetadataFromPdf(PDDocument pdf) { public PdfMetadata extractMetadataFromPdf(PDDocument pdf) {
@@ -64,8 +61,10 @@ public class PdfMetadataService {
String creator = stirlingPDFLabel; String creator = stirlingPDFLabel;
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata() if (applicationProperties
&& runningEE) { .getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator(); creator = applicationProperties.getEnterpriseEdition().getCustomMetadata().getCreator();
pdf.getDocumentInformation().setProducer(stirlingPDFLabel); pdf.getDocumentInformation().setProducer(stirlingPDFLabel);
@@ -84,8 +83,10 @@ public class PdfMetadataService {
pdf.getDocumentInformation().setModificationDate(Calendar.getInstance()); pdf.getDocumentInformation().setModificationDate(Calendar.getInstance());
String author = pdfMetadata.getAuthor(); String author = pdfMetadata.getAuthor();
if (applicationProperties.getEnterpriseEdition().getCustomMetadata().isAutoUpdateMetadata() if (applicationProperties
&& runningEE) { .getEnterpriseEdition()
.getCustomMetadata()
.isAutoUpdateMetadata()) {
author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor(); author = applicationProperties.getEnterpriseEdition().getCustomMetadata().getAuthor();
if (userService != null) { if (userService != null) {

View File

@@ -15,7 +15,6 @@ import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.posthog.java.PostHog; import com.posthog.java.PostHog;
@@ -27,25 +26,19 @@ import stirling.software.SPDF.model.ApplicationProperties;
public class PostHogService { public class PostHogService {
private final PostHog postHog; private final PostHog postHog;
private final String uniqueId; private final String uniqueId;
private final String appVersion;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService; private final UserServiceInterface userService;
private final Environment env;
@Autowired @Autowired
public PostHogService( public PostHogService(
PostHog postHog, PostHog postHog,
@Qualifier("UUID") String uuid, @Qualifier("UUID") String uuid,
@Qualifier("appVersion") String appVersion,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
@Autowired(required = false) UserServiceInterface userService, @Autowired(required = false) UserServiceInterface userService) {
Environment env) {
this.postHog = postHog; this.postHog = postHog;
this.uniqueId = uuid; this.uniqueId = uuid;
this.appVersion = appVersion;
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.userService = userService; this.userService = userService;
this.env = env;
captureSystemInfo(); captureSystemInfo();
} }
@@ -71,16 +64,6 @@ public class PostHogService {
Map<String, Object> metrics = new HashMap<>(); Map<String, Object> metrics = new HashMap<>();
try { try {
// Application version
metrics.put("app_version", appVersion);
String deploymentType = "JAR"; // default
if ("true".equalsIgnoreCase(env.getProperty("BROWSER_OPEN"))) {
deploymentType = "EXE";
} else if (isRunningInDocker()) {
deploymentType = "DOCKER";
}
metrics.put("deployment_type", deploymentType);
// System info // System info
metrics.put("os_name", System.getProperty("os.name")); metrics.put("os_name", System.getProperty("os.name"));
metrics.put("os_version", System.getProperty("os.version")); metrics.put("os_version", System.getProperty("os.version"));
@@ -149,6 +132,7 @@ public class PostHogService {
// Docker detection and stats // Docker detection and stats
boolean isDocker = isRunningInDocker(); boolean isDocker = isRunningInDocker();
metrics.put("is_docker", isDocker);
if (isDocker) { if (isDocker) {
metrics.put("docker_metrics", getDockerMetrics()); metrics.put("docker_metrics", getDockerMetrics());
} }

View File

@@ -105,7 +105,7 @@ public class FileToPdf {
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) { new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry(); ZipEntry entry = zipIn.getNextEntry();
while (entry != null) { while (entry != null) {
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName())); Path filePath = tempUnzippedDir.resolve(entry.getName());
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent()); Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html") if (entry.getName().toLowerCase().endsWith(".html")
@@ -175,7 +175,7 @@ public class FileToPdf {
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) { ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry = zipIn.getNextEntry(); ZipEntry entry = zipIn.getNextEntry();
while (entry != null) { while (entry != null) {
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName())); Path filePath = tempDirectory.resolve(entry.getName());
if (entry.isDirectory()) { if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure Files.createDirectories(filePath); // Explicitly create the directory structure
} else { } else {
@@ -241,14 +241,4 @@ public class FileToPdf {
Files.deleteIfExists(tempOutputFile); Files.deleteIfExists(tempOutputFile);
} }
} }
static String sanitizeZipFilename(String entryName) {
if (entryName == null || entryName.trim().isEmpty()) {
return entryName;
}
while (entryName.contains("../") || entryName.contains("..\\")) {
entryName = entryName.replace("../", "").replace("..\\", "");
}
return entryName;
}
} }

View File

@@ -88,45 +88,15 @@ public class GeneralUtils {
public static boolean isURLReachable(String urlStr) { public static boolean isURLReachable(String urlStr) {
try { try {
// Parse the URL
URL url = URI.create(urlStr).toURL(); URL url = URI.create(urlStr).toURL();
// Allow only http and https protocols
String protocol = url.getProtocol();
if (!protocol.equals("http") && !protocol.equals("https")) {
return false; // Disallow other protocols
}
// Check if the host is a local address
String host = url.getHost();
if (isLocalAddress(host)) {
return false; // Exclude local addresses
}
// Check if the URL is reachable
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("HEAD"); connection.setRequestMethod("HEAD");
// connection.setConnectTimeout(5000); // Set connection timeout
// connection.setReadTimeout(5000); // Set read timeout
int responseCode = connection.getResponseCode(); int responseCode = connection.getResponseCode();
return (200 <= responseCode && responseCode <= 399); return (200 <= responseCode && responseCode <= 399);
} catch (Exception e) { } catch (MalformedURLException e) {
return false; // Return false in case of any exception return false;
} } catch (IOException e) {
} return false;
private static boolean isLocalAddress(String host) {
try {
// Resolve DNS to IP address
InetAddress address = InetAddress.getByName(host);
// Check for local addresses
return address.isAnyLocalAddress() || // Matches 0.0.0.0 or similar
address.isLoopbackAddress() || // Matches 127.0.0.1 or ::1
address.isSiteLocalAddress() || // Matches private IPv4 ranges: 192.168.x.x, 10.x.x.x, 172.16.x.x to 172.31.x.x
address.getHostAddress().startsWith("fe80:"); // Matches link-local IPv6 addresses
} catch (Exception e) {
return false; // Return false for invalid or unresolved addresses
} }
} }
@@ -319,10 +289,6 @@ public class GeneralUtils {
saveKeyToConfig(id, key, true); saveKeyToConfig(id, key, true);
} }
public static void saveKeyToConfig(String id, boolean key) throws IOException {
saveKeyToConfig(id, key, true);
}
public static void saveKeyToConfig(String id, String key, boolean autoGenerated) public static void saveKeyToConfig(String id, String key, boolean autoGenerated)
throws IOException { throws IOException {
Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml
@@ -341,24 +307,6 @@ public class GeneralUtils {
settingsYml.save(); settingsYml.save();
} }
public static void saveKeyToConfig(String id, boolean key, boolean autoGenerated)
throws IOException {
Path path = Paths.get("configs", "settings.yml");
final YamlFile settingsYml = new YamlFile(path.toFile());
DumperOptions yamlOptionssettingsYml =
((SimpleYamlImplementation) settingsYml.getImplementation()).getDumperOptions();
yamlOptionssettingsYml.setSplitLines(false);
settingsYml.loadWithComments();
YamlFileWrapper writer = settingsYml.path(id).set(key);
if (autoGenerated) {
writer.comment("# Automatically Generated Settings (Do Not Edit Directly)");
}
settingsYml.save();
}
public static String generateMachineFingerprint() { public static String generateMachineFingerprint() {
try { try {
// Get the MAC address // Get the MAC address
@@ -401,33 +349,4 @@ public class GeneralUtils {
return "GenericID"; return "GenericID";
} }
} }
public static boolean isVersionHigher(String currentVersion, String compareVersion) {
if (currentVersion == null || compareVersion == null) {
return false;
}
// Split versions into components
String[] current = currentVersion.split("\\.");
String[] compare = compareVersion.split("\\.");
// Get the length of the shorter version array
int length = Math.min(current.length, compare.length);
// Compare each component
for (int i = 0; i < length; i++) {
int currentPart = Integer.parseInt(current[i]);
int comparePart = Integer.parseInt(compare[i]);
if (currentPart > comparePart) {
return true;
}
if (currentPart < comparePart) {
return false;
}
}
// If all components so far are equal, the longer version is considered higher
return current.length > compare.length;
}
} }

View File

@@ -29,12 +29,12 @@ public class ProcessExecutor {
public enum Processes { public enum Processes {
LIBRE_OFFICE, LIBRE_OFFICE,
PDFTOHTML, PDFTOHTML,
OCR_MY_PDF,
PYTHON_OPENCV, PYTHON_OPENCV,
GHOSTSCRIPT,
WEASYPRINT, WEASYPRINT,
INSTALL_APP, INSTALL_APP,
CALIBRE, CALIBRE
TESSERACT,
QPDF
} }
private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>(); private static final Map<Processes, ProcessExecutor> instances = new ConcurrentHashMap<>();
@@ -59,11 +59,21 @@ public class ProcessExecutor {
.getProcessExecutor() .getProcessExecutor()
.getSessionLimit() .getSessionLimit()
.getPdfToHtmlSessionLimit(); .getPdfToHtmlSessionLimit();
case OCR_MY_PDF ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getOcrMyPdfSessionLimit();
case PYTHON_OPENCV -> case PYTHON_OPENCV ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()
.getSessionLimit() .getSessionLimit()
.getPythonOpenCvSessionLimit(); .getPythonOpenCvSessionLimit();
case GHOSTSCRIPT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getGhostScriptSessionLimit();
case WEASYPRINT -> case WEASYPRINT ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()
@@ -74,16 +84,6 @@ public class ProcessExecutor {
.getProcessExecutor() .getProcessExecutor()
.getSessionLimit() .getSessionLimit()
.getInstallAppSessionLimit(); .getInstallAppSessionLimit();
case TESSERACT ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getTesseractSessionLimit();
case QPDF ->
applicationProperties
.getProcessExecutor()
.getSessionLimit()
.getQpdfSessionLimit();
case CALIBRE -> case CALIBRE ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()
@@ -103,11 +103,21 @@ public class ProcessExecutor {
.getProcessExecutor() .getProcessExecutor()
.getTimeoutMinutes() .getTimeoutMinutes()
.getPdfToHtmlTimeoutMinutes(); .getPdfToHtmlTimeoutMinutes();
case OCR_MY_PDF ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getOcrMyPdfTimeoutMinutes();
case PYTHON_OPENCV -> case PYTHON_OPENCV ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()
.getTimeoutMinutes() .getTimeoutMinutes()
.getPythonOpenCvTimeoutMinutes(); .getPythonOpenCvTimeoutMinutes();
case GHOSTSCRIPT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getGhostScriptTimeoutMinutes();
case WEASYPRINT -> case WEASYPRINT ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()
@@ -118,16 +128,6 @@ public class ProcessExecutor {
.getProcessExecutor() .getProcessExecutor()
.getTimeoutMinutes() .getTimeoutMinutes()
.getInstallAppTimeoutMinutes(); .getInstallAppTimeoutMinutes();
case TESSERACT ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getTesseractTimeoutMinutes();
case QPDF ->
applicationProperties
.getProcessExecutor()
.getTimeoutMinutes()
.getQpdfTimeoutMinutes();
case CALIBRE -> case CALIBRE ->
applicationProperties applicationProperties
.getProcessExecutor() .getProcessExecutor()

View File

@@ -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");
}
}
}

View File

@@ -3,10 +3,6 @@ multipart.enabled=true
logging.level.org.springframework=WARN logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN logging.level.org.hibernate=WARN
logging.level.org.eclipse.jetty=WARN logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.saml2=TRACE
#logging.level.org.springframework.security=DEBUG
#logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.SPDF.config.security: DEBUG
logging.level.com.zaxxer.hikari=WARN logging.level.com.zaxxer.hikari=WARN
spring.jpa.open-in-view=false spring.jpa.open-in-view=false
@@ -31,8 +27,6 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
spring.devtools.restart.enabled=true spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json
@@ -41,7 +35,7 @@ spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
#spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/ #spring.thymeleaf.prefix=file:/customFiles/templates/,classpath:/templates/
#spring.thymeleaf.cache=false #spring.thymeleaf.cache=false
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-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.driver-class-name=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=

File diff suppressed because it is too large Load Diff

View File

@@ -81,11 +81,11 @@ page=صفحة
pages=صفحات pages=صفحات
loading=جارٍ التحميل... loading=جارٍ التحميل...
addToDoc=إضافة إلى المستند addToDoc=إضافة إلى المستند
reset=إعداة ضبط reset=Reset
legal.privacy=سياسة الخصوصية legal.privacy=سياسة الخصوصية
legal.terms=شروط الاستخدام legal.terms=شروط الاستخدام
legal.accessibility=إمكانية الوصول legal.accessibility=Accessibility
legal.cookie=سياسة ملفات تعريف الارتباط legal.cookie=سياسة ملفات تعريف الارتباط
legal.impressum=بيان الهوية legal.impressum=بيان الهوية
@@ -119,8 +119,8 @@ pipelineOptions.validateButton=تحقق
######################## ########################
enterpriseEdition.button=ترقية إلى محترف enterpriseEdition.button=ترقية إلى محترف
enterpriseEdition.warning=هذه الخاصية متوفرة فقط للمستخدمين المحترفين. enterpriseEdition.warning=هذه الخاصية متوفرة فقط للمستخدمين المحترفين.
enterpriseEdition.yamlAdvert=يدعم Stirling PDF Pro ملفات الإعدادات YAML وميزات SSO أخرى enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=هل تبحث عن المزيد من ميزات إدارة المستخدمين؟ اطلع على Stirling PDF Pro enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
################# #################
@@ -142,7 +142,7 @@ navbar.language=اللغات
navbar.settings=إعدادات navbar.settings=إعدادات
navbar.allTools=أدوات navbar.allTools=أدوات
navbar.multiTool=أدوات متعددة navbar.multiTool=أدوات متعددة
navbar.search=البحث navbar.search=Search
navbar.sections.organize=تنظيم navbar.sections.organize=تنظيم
navbar.sections.convertTo=تحويل الى PDF navbar.sections.convertTo=تحويل الى PDF
navbar.sections.convertFrom=تحويل من PDF navbar.sections.convertFrom=تحويل من PDF
@@ -247,8 +247,8 @@ database.fileNotFound=لم يتم العثور على الملف
database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا database.fileNullOrEmpty=يجب ألا يكون الملف فارغًا أو خاليًا
database.failedImportFile=فشل استيراد الملف database.failedImportFile=فشل استيراد الملف
session.expired=لقد انتهت جلستك. يرجى تحديث الصفحة والمحاولة مرة أخرى session.expired=Your session has expired. Please refresh the page and try again.
session.refreshPage=تحديث الصفحة session.refreshPage=Refresh Page
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -512,20 +512,16 @@ home.splitPdfByChapters.title=تجزئة المستندات PDF حسب الفص
home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله. home.splitPdfByChapters.desc=قسم مستند PDF إلى ملفات متعددة بناءً على هيكل فصوله.
splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم splitPdfByChapters.tags=تجزئة، فصول، علامات تبويب، تنظيم
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=إستبدال-عكس اللون replace-color.title=Replace-Invert-Color
replace-color.header=استبدال-عكس لون PDF replace-color.header=استبدال-إلغاء مirro لون PDF
home.replaceColorPdf.title=إستبدال و عكس الألوان home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف home.replaceColorPdf.desc=استبدال الألوان للنصوص والخلفيات في المستندات PDF وإلغاء تعكير اللون الكامل للمستند لتقليل حجم الملف
replaceColorPdf.tags=استبدال اللون، عمليات الصفحة، الخلفية، جانب الخادم replaceColorPdf.tags=استبدال اللون، عمليات الصفحة، الخلفية، جانب الخادم
replace-color.selectText.1=خيارات استبدال أو عكس الألوان replace-color.selectText.1=خيارات استبدال-إلغاء مirro لون
replace-color.selectText.2=افتراضي(ألوان التباين العالي الافتراضية) replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=خصيصة (ألوان شخصية) replace-color.selectText.3=خصيصة (ألوان شخصية)
replace-color.selectText.4=عكس كامل(عكس جميع الألوان) replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=خيارات ألوان التباين العالي replace-color.selectText.5=خيارات ألوان التباين العالي
replace-color.selectText.6=نص أبيض على خلفية سوداء replace-color.selectText.6=نص أبيض على خلفية سوداء
replace-color.selectText.7=نص أسود على خلفية بيضاء replace-color.selectText.7=نص أسود على خلفية بيضاء
@@ -822,12 +818,7 @@ sign.save=حفظ توقيع
sign.personalSigs=توقيعات شخصية sign.personalSigs=توقيعات شخصية
sign.sharedSigs=توقيعات مشتركة sign.sharedSigs=توقيعات مشتركة
sign.noSavedSigs=لم يتم العثور على توقيعات محفوظة sign.noSavedSigs=لم يتم العثور على توقيعات محفوظة
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=إصلاح repair.title=إصلاح
@@ -872,7 +863,7 @@ ocr.selectText.10=وضع التعرف الضوئي على الحروف
ocr.selectText.11=إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل) ocr.selectText.11=إزالة الصور بعد التعرف الضوئي على الحروف (يزيل كل الصور، يكون مفيدًا فقط إذا كان جزءًا من خطوة التحويل)
ocr.selectText.12=نوع العرض (متقدم) ocr.selectText.12=نوع العرض (متقدم)
ocr.help=يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker ocr.help=يرجى قراءة هذه الوثائق حول كيفية استخدام هذا للغات أخرى و/أو الاستخدام ليس في Docker
ocr.credit=تستخدم هذه الخدمة qpdf و Tesseract للتعرف الضوئي على الحروف. ocr.credit=تستخدم هذه الخدمة OCRmyPDF و Tesseract للتعرف الضوئي على الحروف.
ocr.submit=معالجة PDF باستخدام OCR ocr.submit=معالجة PDF باستخدام OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=تحويل إلى PDF
#compress #compress
compress.title=ضغط compress.title=ضغط
compress.header=ضغط ملف PDF compress.header=ضغط ملف PDF
compress.credit=تستخدم هذه الخدمة qpdf لضغط / تحسين PDF. compress.credit=تستخدم هذه الخدمة OCRmyPDF لضغط / تحسين PDF.
compress.selectText.1=الوضع اليدوي - من 1 إلى 4 compress.selectText.1=الوضع اليدوي - من 1 إلى 4
compress.selectText.2=مستوى التحسين: compress.selectText.2=مستوى التحسين:
compress.selectText.3=4 (رهيب للصور النصية) compress.selectText.3=4 (رهيب للصور النصية)
@@ -944,39 +935,17 @@ pdfOrganiser.placeholder=(مثال: 1,3,2 أو 4-8,2,10-12 أو 2n-1)
multiTool.title=أداة متعددة PDF multiTool.title=أداة متعددة PDF
multiTool.header=أداة متعددة PDF multiTool.header=أداة متعددة PDF
multiTool.uploadPrompts=اسم الملف multiTool.uploadPrompts=اسم الملف
multiTool.selectAll=تحديد الكل multiTool.selectAll=Select All
multiTool.deselectAll=إلغاء تحديد الكل multiTool.deselectAll=Deselect All
multiTool.selectPages=تحديد الصفحة multiTool.selectPages=Page Select
multiTool.selectedPages=الصفحات المحددة multiTool.selectedPages=Selected Pages
multiTool.page=صفحة multiTool.page=Page
multiTool.deleteSelected=حذف المحدد multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=تصدير multiTool.downloadAll=Export
multiTool.downloadSelected=تصدير المحدد multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=إدراج فاصل صفحات
multiTool.addFile=إضافة ملف
multiTool.rotateLeft=تدوير إلى اليسار
multiTool.rotateRight=تدوير إلى اليمين
multiTool.split=تقسيم
multiTool.moveLeft=تحريك إلى اليسار
multiTool.moveRight=تحريك إلى اليمين
multiTool.delete=حذف
multiTool.dragDropMessage=الصفحات المحددة
multiTool.undo=تراجع
multiTool.redo=إعادة إجراء
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=هذه الميزة متوفرة في <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 #view pdf
viewPdf.title=عرض PDF viewPdf.title=عرض PDF
@@ -1066,7 +1035,6 @@ addPassword.submit=تشفير
#watermark #watermark
watermark.title=إضافة علامة مائية watermark.title=إضافة علامة مائية
watermark.header=إضافة علامة مائية watermark.header=إضافة علامة مائية
watermark.customColor=لون نص مخصص
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه: watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
watermark.selectText.2=نص العلامة المائية: watermark.selectText.2=نص العلامة المائية:
watermark.selectText.3=حجم الخط: watermark.selectText.3=حجم الخط:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=تغيير
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF إلى PDF/A pdfToPDFA.title=PDF إلى PDF/A
pdfToPDFA.header=PDF إلى PDF/A pdfToPDFA.header=PDF إلى PDF/A
pdfToPDFA.credit=تستخدم هذه الخدمة qpdf لتحويل PDF/A. pdfToPDFA.credit=تستخدم هذه الخدمة ghostscript لتحويل PDF/A.
pdfToPDFA.submit=تحويل pdfToPDFA.submit=تحويل
pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد pdfToPDFA.tip=لا يعمل حاليًا لمدخلات متعددة في وقت واحد
pdfToPDFA.outputFormat=تنسيق الإخراج pdfToPDFA.outputFormat=تنسيق الإخراج
@@ -1271,57 +1239,9 @@ splitByChapters.title=تجزئة المستند حسب الفصول
splitByChapters.header=تجزئة المستند حسب الفصول splitByChapters.header=تجزئة المستند حسب الفصول
splitByChapters.bookmarkLevel=مستوى العلامات التذكارية splitByChapters.bookmarkLevel=مستوى العلامات التذكارية
splitByChapters.includeMetadata=شامل البيانات المرفقة splitByChapters.includeMetadata=شامل البيانات المرفقة
splitByChapters.allowDuplicates=السماح بالتكرار splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=هذه الأداة تقوم بتقسيم ملف PDF إلى عدة ملفات PDF استناداً إلى بنية فصوله splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=مستوى الإشارة المرجعية: اختر مستوى الإشارات المرجعية التي تريد استخدامها للتقسيم (0 للمستوى الأعلى، 1 للمستوى الثاني، وما إلى ذلك) 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.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة. splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
splitByChapters.submit=تقطيع ملف PDF splitByChapters.submit=تقطيع ملف PDF
#File Chooser
fileChooser.click=انقر هنا
fileChooser.or=أو
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

File diff suppressed because it is too large Load Diff

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Разделете PDF по глави
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави. home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
splitPdfByChapters.tags=разделяне, глави, отметки, организиране splitPdfByChapters.tags=разделяне, глави, отметки, организиране
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Замени-инвертиране-на-цвят replace-color.title=Замени-инвертиране-на-цвят
replace-color.header=Замяна-инвертиране на цвят PDF replace-color.header=Замяна-инвертиране на цвят PDF
@@ -822,12 +818,7 @@ sign.save=Save Signature
sign.personalSigs=Personal Signatures sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Поправи repair.title=Поправи
@@ -872,7 +863,7 @@ ocr.selectText.10=OCR режим
ocr.selectText.11=Премахване на изображения след OCR (Премахва ВСИЧКИ изображения, полезно само ако е част от стъпката на преобразуване) ocr.selectText.11=Премахване на изображения след OCR (Премахва ВСИЧКИ изображения, полезно само ако е част от стъпката на преобразуване)
ocr.selectText.12=Тип изобразяване (Разширен) ocr.selectText.12=Тип изобразяване (Разширен)
ocr.help=Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker ocr.help=Моля, прочетете тази документация за това как да използвате това за други езици и/или да не използвате в docker
ocr.credit=Тази услуга използва qpdf и Tesseract за OCR. ocr.credit=Тази услуга използва OCRmyPDF и Tesseract за OCR.
ocr.submit=Обработка на PDF чрез OCR ocr.submit=Обработка на PDF чрез OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Преобразуване към PDF
#compress #compress
compress.title=Компресиране compress.title=Компресиране
compress.header=Компресиране на PDF compress.header=Компресиране на PDF
compress.credit=Тази услуга използва qpdf за PDF компресиране/оптимизиране. compress.credit=Тази услуга използва Ghostscript за PDF компресиране/оптимизиране.
compress.selectText.1=Ръчен режим - от 1 до 4 compress.selectText.1=Ръчен режим - от 1 до 4
compress.selectText.2=Ниво на оптимизация: compress.selectText.2=Ниво на оптимизация:
compress.selectText.3=4 (Ужасно за текстови изображения) compress.selectText.3=4 (Ужасно за текстови изображения)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Шифроване
#watermark #watermark
watermark.title=Добавяне на воден знак watermark.title=Добавяне на воден знак
watermark.header=Добавяне на воден знак watermark.header=Добавяне на воден знак
watermark.customColor=Персонализиран цвят на текста
watermark.selectText.1=Изберете PDF, към който да добавите воден знак: watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
watermark.selectText.2=Текст на воден знак: watermark.selectText.2=Текст на воден знак:
watermark.selectText.3=Размер на шрифта: watermark.selectText.3=Размер на шрифта:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Промени
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF към PDF/A pdfToPDFA.title=PDF към PDF/A
pdfToPDFA.header=PDF към PDF/A pdfToPDFA.header=PDF към PDF/A
pdfToPDFA.credit=Тази услуга използва qpdf за PDF/A преобразуване. pdfToPDFA.credit=Тази услуга използва ghostscript за PDF/A преобразуване.
pdfToPDFA.submit=Преобразуване pdfToPDFA.submit=Преобразуване
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
pdfToPDFA.outputFormat=Изходен формат pdfToPDFA.outputFormat=Изходен формат
@@ -1277,51 +1245,3 @@ splitByChapters.desc.2=Ниво на отметка: Изберете нивот
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF. splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове. splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Разделяне на PDF splitByChapters.submit=Разделяне на PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -512,10 +512,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. home.splitPdfByChapters.desc=Divideix un PDF en múltiples fitxers segons la seva estructura de capítols.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Reemplaça-Inverteix-Color replace-color.title=Reemplaça-Inverteix-Color
replace-color.header=Reemplaça-Inverteix Color en PDF replace-color.header=Reemplaça-Inverteix Color en PDF
@@ -822,12 +818,7 @@ sign.save=Desa Signatura
sign.personalSigs=Signatures Personals sign.personalSigs=Signatures Personals
sign.sharedSigs=Signatures Compartides sign.sharedSigs=Signatures Compartides
sign.noSavedSigs=No s'han trobat signatures desades sign.noSavedSigs=No s'han trobat signatures desades
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Reparar repair.title=Reparar
@@ -872,7 +863,7 @@ ocr.selectText.10=Mode OCR
ocr.selectText.11=Elimina Imatges després de l'OCR (Elimina TOTES les imatges, útil si forma part d'un procés de conversió) ocr.selectText.11=Elimina Imatges després de l'OCR (Elimina TOTES les imatges, útil si forma part d'un procés de conversió)
ocr.selectText.12=Tipus de Renderització (Avançat) ocr.selectText.12=Tipus de Renderització (Avançat)
ocr.help=Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker ocr.help=Llegeix aquesta documentació sobre com utilitzar-la per a altres idiomes i/o no utilitzar-la a Docker
ocr.credit=Aquest servei fa servir qpdf i Tesseract per a OCR. ocr.credit=Aquest servei fa servir OCRmyPDF i Tesseract per a OCR.
ocr.submit=Processa PDF amb OCR ocr.submit=Processa PDF amb OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Converteix a PDF
#compress #compress
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF compress.header=Comprimir PDF
compress.credit=Aquest servei utilitza 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.1=Mode manual: de l'1 al 4
compress.selectText.2=Nivell d'optimització: compress.selectText.2=Nivell d'optimització:
compress.selectText.3=4 (terrible per a imatges de text) compress.selectText.3=4 (terrible per a imatges de text)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Encripta
#watermark #watermark
watermark.title=Afegir Marca d'Aigua watermark.title=Afegir Marca d'Aigua
watermark.header=Afegir Marca d'Aigua watermark.header=Afegir Marca d'Aigua
watermark.customColor=Color de Text Personalitzat
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua: watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
watermark.selectText.2=Text de la Marca d'Aigua watermark.selectText.2=Text de la Marca d'Aigua
watermark.selectText.3=Mida de la Font: watermark.selectText.3=Mida de la Font:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Canvia
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF a PDF/A pdfToPDFA.title=PDF a PDF/A
pdfToPDFA.header=PDF a PDF/A pdfToPDFA.header=PDF a PDF/A
pdfToPDFA.credit=Utilitza qpdf per a la conversió a PDF/A pdfToPDFA.credit=Utilitza Ghostscript per a la conversió a PDF/A
pdfToPDFA.submit=Converteix pdfToPDFA.submit=Converteix
pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps pdfToPDFA.tip=Actualment no funciona per a múltiples entrades al mateix temps
pdfToPDFA.outputFormat=Format de sortida pdfToPDFA.outputFormat=Format de sortida
@@ -1277,51 +1245,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.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit.
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats. splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
splitByChapters.submit=Divideix PDF splitByChapters.submit=Divideix PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -512,10 +512,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. home.splitPdfByChapters.desc=Rozdělit PDF do více souborů na základě jeho struktury kapitol.
splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace splitPdfByChapters.tags=rozdělení, kapitoly, zápisky, organizace
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Nahradit inverzní barvu PDF replace-color.header=Nahradit inverzní barvu PDF
@@ -822,12 +818,7 @@ sign.save=Uložit podpis
sign.personalSigs=Osobní podpisy sign.personalSigs=Osobní podpisy
sign.sharedSigs=Sdílené podpisy sign.sharedSigs=Sdílené podpisy
sign.noSavedSigs=Nenašly se žádné uložené podpisy sign.noSavedSigs=Nenašly se žádné uložené podpisy
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Opravit repair.title=Opravit
@@ -872,7 +863,7 @@ ocr.selectText.10=Režim OCR
ocr.selectText.11=Odstranit obrázky po OCR (Odstraní VŠECHNY obrázky, užitečné pouze jako součást kroku konverze) ocr.selectText.11=Odstranit obrázky po OCR (Odstraní VŠECHNY obrázky, užitečné pouze jako součást kroku konverze)
ocr.selectText.12=Typ vykreslení (Pokročilé) ocr.selectText.12=Typ vykreslení (Pokročilé)
ocr.help=Prosím, přečtěte si tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker ocr.help=Prosím, přečtěte si tuto dokumentaci o použití pro jiné jazyky a/nebo použití mimo Docker
ocr.credit=Tato služba používá qpdf a Tesseract pro OCR. ocr.credit=Tato služba používá OCRmyPDF a Tesseract pro OCR.
ocr.submit=Zpracovat PDF s OCR ocr.submit=Zpracovat PDF s OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Převést na PDF
#compress #compress
compress.title=Komprese compress.title=Komprese
compress.header=Komprimovat PDF compress.header=Komprimovat PDF
compress.credit=Tato služba používá 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.1=Ruční režim - Od 1 do 4
compress.selectText.2=Úroveň optimalizace: compress.selectText.2=Úroveň optimalizace:
compress.selectText.3=4 (Hrozné pro textové obrázky) compress.selectText.3=4 (Hrozné pro textové obrázky)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Šifrovat
#watermark #watermark
watermark.title=Přidat vodoznak watermark.title=Přidat vodoznak
watermark.header=Přidat vodoznak watermark.header=Přidat vodoznak
watermark.customColor=Vlastní barva textu
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak: watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
watermark.selectText.2=Text vodoznaku: watermark.selectText.2=Text vodoznaku:
watermark.selectText.3=Velikost písma: watermark.selectText.3=Velikost písma:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Změnit
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF na PDF/A pdfToPDFA.title=PDF na PDF/A
pdfToPDFA.header=PDF na PDF/A pdfToPDFA.header=PDF na PDF/A
pdfToPDFA.credit=Tato služba používá 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.submit=Převést
pdfToPDFA.tip=V současné době nepracuje pro více vstupů najednou pdfToPDFA.tip=V současné době nepracuje pro více vstupů najednou
pdfToPDFA.outputFormat=Výstupní formát pdfToPDFA.outputFormat=Výstupní formát
@@ -1277,51 +1245,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.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru.
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně. splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
splitByChapters.submit=Podělit se PDF splitByChapters.submit=Podělit se PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Partitioner PDF efter kapitler
home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur. home.splitPdfByChapters.desc=Partitioner en PDF i flere filer baseret på dens kapitelstruktur.
splitPdfByChapters.tags=partitionering,kapitler,merker,organisering splitPdfByChapters.tags=partitionering,kapitler,merker,organisering
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Erstat-omgivende Farve PDF replace-color.header=Erstat-omgivende Farve PDF
@@ -822,12 +818,7 @@ sign.save=Gem Signatur
sign.personalSigs=Personlige Signaturer sign.personalSigs=Personlige Signaturer
sign.sharedSigs=Delte Signaturer sign.sharedSigs=Delte Signaturer
sign.noSavedSigs=Ingen Gemte Signaturer Fundet sign.noSavedSigs=Ingen Gemte Signaturer Fundet
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Reparér repair.title=Reparér
@@ -872,7 +863,7 @@ ocr.selectText.10=OCR-tilstand
ocr.selectText.11=Fjern billeder efter OCR (Fjerner ALLE billeder, kun nyttigt hvis det er en del af konverteringstrinnet) ocr.selectText.11=Fjern billeder efter OCR (Fjerner ALLE billeder, kun nyttigt hvis det er en del af konverteringstrinnet)
ocr.selectText.12=Renderingstype (Avanceret) ocr.selectText.12=Renderingstype (Avanceret)
ocr.help=Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker ocr.help=Læs venligst denne dokumentation om, hvordan man bruger dette til andre sprog og/eller brug uden for docker
ocr.credit=Denne tjeneste bruger qpdf og Tesseract til OCR. ocr.credit=Denne tjeneste bruger OCRmyPDF og Tesseract til OCR.
ocr.submit=Behandl PDF med OCR ocr.submit=Behandl PDF med OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Konvertér til PDF
#compress #compress
compress.title=Komprimer compress.title=Komprimer
compress.header=Komprimer PDF compress.header=Komprimer PDF
compress.credit=Denne tjeneste bruger 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.1=Manuel Tilstand - Fra 1 til 4
compress.selectText.2=Optimeringsniveau: compress.selectText.2=Optimeringsniveau:
compress.selectText.3=4 (Forfærdelig for tekstbilleder) compress.selectText.3=4 (Forfærdelig for tekstbilleder)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Kryptér
#watermark #watermark
watermark.title=Tilføj Vandmærke watermark.title=Tilføj Vandmærke
watermark.header=Tilføj Vandmærke watermark.header=Tilføj Vandmærke
watermark.customColor=Brugerdefineret Tekstfarve
watermark.selectText.1=Vælg PDF til at tilføje vandmærke: watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
watermark.selectText.2=Vandmærketekst: watermark.selectText.2=Vandmærketekst:
watermark.selectText.3=Skriftstørrelse: watermark.selectText.3=Skriftstørrelse:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Ændre
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF Til PDF/A pdfToPDFA.title=PDF Til PDF/A
pdfToPDFA.header=PDF Til PDF/A pdfToPDFA.header=PDF Til PDF/A
pdfToPDFA.credit=Denne tjeneste bruger qpdf til PDF/A-konvertering pdfToPDFA.credit=Denne tjeneste bruger ghostscript til PDF/A-konvertering
pdfToPDFA.submit=Konvertér pdfToPDFA.submit=Konvertér
pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang pdfToPDFA.tip=Fungerer i øjeblikket ikke for flere input på én gang
pdfToPDFA.outputFormat=Outputformat pdfToPDFA.outputFormat=Outputformat
@@ -1277,51 +1245,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.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF.
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er. splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
splitByChapters.submit=Splitter PDF splitByChapters.submit=Splitter PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -81,7 +81,7 @@ page=Seite
pages=Seiten pages=Seiten
loading=Laden... loading=Laden...
addToDoc=In Dokument hinzufügen addToDoc=In Dokument hinzufügen
reset=Zurücksetzen reset=Reset
legal.privacy=Datenschutz legal.privacy=Datenschutz
legal.terms=AGB legal.terms=AGB
@@ -146,8 +146,8 @@ navbar.search=Suche
navbar.sections.organize=Organisieren navbar.sections.organize=Organisieren
navbar.sections.convertTo=In PDF konvertieren navbar.sections.convertTo=In PDF konvertieren
navbar.sections.convertFrom=Konvertieren von PDF navbar.sections.convertFrom=Konvertieren von PDF
navbar.sections.security=Signieren und Sicherheit navbar.sections.security=Zeichen und Sicherheit
navbar.sections.advance=Erweiterte Funktionen navbar.sections.advance=Fortschrittlich
navbar.sections.edit=Anzeigen und Bearbeiten navbar.sections.edit=Anzeigen und Bearbeiten
navbar.sections.popular=Beliebt navbar.sections.popular=Beliebt
@@ -248,7 +248,7 @@ database.fileNullOrEmpty=Datei darf nicht null oder leer sein
database.failedImportFile=Dateiimport fehlgeschlagen database.failedImportFile=Dateiimport fehlgeschlagen
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut. session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
session.refreshPage=Seite aktualisieren session.refreshPage=Refresh Page
############# #############
# HOME-PAGE # # HOME-PAGE #
@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=PDF-Datei nach Kapiteln aufteilen
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur. home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
home.validateSignature.title=PDF-Signatur überprüfen
home.validateSignature.desc=Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen
validateSignature.tags=signature,verify,validate,pdf,digitale signatur,signatur validieren,überprüfen,Zertifikat,cert
#replace-invert-color #replace-invert-color
replace-color.title=Farbe Ersetzen-Invertieren replace-color.title=Farbe Ersetzen-Invertieren
replace-color.header=Farb-PDF Ersetzen-Invertieren replace-color.header=Farb-PDF Ersetzen-Invertieren
@@ -822,12 +818,7 @@ sign.save=Signature speichern
sign.personalSigs=Persönliche Signaturen sign.personalSigs=Persönliche Signaturen
sign.sharedSigs=Geteilte Signaturen sign.sharedSigs=Geteilte Signaturen
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
sign.addToAll=Zu allen Seiten hinzufügen
sign.delete=Löschen
sign.first=Erste Seite
sign.last=Letzte Seite
sign.next=Nächste Seite
sign.previous=Vorherige Seite
#repair #repair
repair.title=Reparieren repair.title=Reparieren
@@ -872,7 +863,7 @@ ocr.selectText.10=OCR-Modus
ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts) ocr.selectText.11=Bilder nach OCR entfernen (Entfernt ALLE Bilder, nur sinnvoll, wenn Teil des Konvertierungsschritts)
ocr.selectText.12=Rendertyp (Erweitert) ocr.selectText.12=Rendertyp (Erweitert)
ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können ocr.help=Bitte lesen Sie diese Dokumentation, um zu erfahren, wie Sie dies für andere Sprachen verwenden und/oder nicht in Docker verwenden können
ocr.credit=Dieser Dienst verwendet qpdf und Tesseract für OCR. ocr.credit=Dieser Dienst verwendet OCRmyPDF und Tesseract für OCR.
ocr.submit=PDF mit OCR verarbeiten ocr.submit=PDF mit OCR verarbeiten
@@ -896,7 +887,7 @@ fileToPDF.submit=In PDF konvertieren
#compress #compress
compress.title=Komprimieren compress.title=Komprimieren
compress.header=PDF komprimieren compress.header=PDF komprimieren
compress.credit=Dieser Dienst verwendet 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.1=Manueller Modus Von 1 bis 4
compress.selectText.2=Optimierungsstufe: compress.selectText.2=Optimierungsstufe:
compress.selectText.3=4 (Schrecklich für Textbilder) compress.selectText.3=4 (Schrecklich für Textbilder)
@@ -953,30 +944,8 @@ multiTool.deleteSelected=Auswahl löschen
multiTool.downloadAll=Downloaden multiTool.downloadAll=Downloaden
multiTool.downloadSelected=Auswahl downloaden multiTool.downloadSelected=Auswahl downloaden
multiTool.insertPageBreak=Seitenumbruch einfügen
multiTool.addFile=Datei hinzufügen
multiTool.rotateLeft=Nach links drehen
multiTool.rotateRight=Nach rechts drehen
multiTool.split=Teilen
multiTool.moveLeft=Nach links verschieben
multiTool.moveRight=Nach rechts verschieben
multiTool.delete=Löschen
multiTool.dragDropMessage=Ausgewählte Seite(n)
multiTool.undo=Rückgängig machen
multiTool.redo=Wiederherstellen
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=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 #view pdf
viewPdf.title=PDF anzeigen viewPdf.title=PDF anzeigen
@@ -1066,7 +1035,6 @@ addPassword.submit=Verschlüsseln
#watermark #watermark
watermark.title=Wasserzeichen hinzufügen watermark.title=Wasserzeichen hinzufügen
watermark.header=Wasserzeichen hinzufügen watermark.header=Wasserzeichen hinzufügen
watermark.customColor=Benutzerdefinierte Textfarbe
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll: watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
watermark.selectText.2=Wasserzeichen Text: watermark.selectText.2=Wasserzeichen Text:
watermark.selectText.3=Schriftgröße: watermark.selectText.3=Schriftgröße:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Ändern
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF zu PDF/A pdfToPDFA.title=PDF zu PDF/A
pdfToPDFA.header=PDF zu PDF/A pdfToPDFA.header=PDF zu PDF/A
pdfToPDFA.credit=Dieser Dienst verwendet qpdf für die PDF/A-Konvertierung pdfToPDFA.credit=Dieser Dienst verwendet ghostscript für die PDF/A-Konvertierung
pdfToPDFA.submit=Konvertieren pdfToPDFA.submit=Konvertieren
pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten. pdfToPDFA.tip=Dieser Dienst kann nur einzelne Eingangsdateien verarbeiten.
pdfToPDFA.outputFormat=Ausgabeformat pdfToPDFA.outputFormat=Ausgabeformat
@@ -1277,51 +1245,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.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen.
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen. splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
splitByChapters.submit=PDF teilen splitByChapters.submit=PDF teilen
#File Chooser
fileChooser.click=Klicken
fileChooser.or=oder
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
#release notes
releases.footer=Veröffentlichungen
releases.title=Versionshinweise
releases.header=Versionshinweise
releases.current.version=Aktuelle Version
releases.note=Versionshinweise sind nur auf Englisch verfügbar
#Validate Signature
validateSignature.title=PDF-Signaturen überprüfen
validateSignature.header=Digitale Signaturen überprüfen
validateSignature.selectPDF=Signierte PDF-Datei auswählen
validateSignature.submit=Signaturen überprüfen
validateSignature.results=Gültigkeitsprüfungsergebnisse
validateSignature.status=Status
validateSignature.signer=Unterzeichner
validateSignature.date=Datum
validateSignature.reason=Grund
validateSignature.location=Ort
validateSignature.noSignatures=Keine digitalen Signaturen in diesem Dokument gefunden
validateSignature.status.valid=Gültig
validateSignature.status.invalid=Ungültig
validateSignature.chain.invalid=Zertifikatskettenprüfung fehlgeschlagen - kann die Identität des Unterzeichners nicht verifizieren
validateSignature.trust.invalid=Zertifikat nicht im Truststore - Quelle kann nicht verifiziert werden
validateSignature.cert.expired=Zertifikat ist abgelaufen
validateSignature.cert.revoked=Zertifikat wurde widerrufen
validateSignature.signature.info=Signaturinformationen
validateSignature.signature=Signatur
validateSignature.signature.mathValid=Signatur ist mathematisch gültig ABER:
validateSignature.selectCustomCert=Benutzerdefinierte Zertifikatsdatei X.509 (Optional)
validateSignature.cert.info=Zertifikat Details
validateSignature.cert.issuer=Aussteller
validateSignature.cert.subject=Betreff
validateSignature.cert.serialNumber=Seriennummer
validateSignature.cert.validFrom=Gültig von
validateSignature.cert.validUntil=Gültig bis
validateSignature.cert.algorithm=Algorithmus
validateSignature.cert.keySize=Schlüsselgröße
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Schlüsselverwendung
validateSignature.cert.selfSigned=Selbstsigniert
validateSignature.cert.bits=bits

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Διχοτομία PDF ανά Περιγραφές
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=διχοτομία,περιγραφές,κεφάλαια,συνορία splitPdfByChapters.tags=διχοτομία,περιγραφές,κεφάλαια,συνορία
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Αντικατάσταση-Αντίστροφη Παθωμένη Πίντσουρ replace-color.header=Αντικατάσταση-Αντίστροφη Παθωμένη Πίντσουρ
@@ -822,12 +818,7 @@ sign.save=Αποθήκευση Αλιάσης
sign.personalSigs=Προσωπικές Αλιάσεις sign.personalSigs=Προσωπικές Αλιάσεις
sign.sharedSigs=Μεταδότες Αλιάσεις sign.sharedSigs=Μεταδότες Αλιάσεις
sign.noSavedSigs=Δεν βρέθηκαν αποθηκευμένες αλιάσεις sign.noSavedSigs=Δεν βρέθηκαν αποθηκευμένες αλιάσεις
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Επιδιόρθωση repair.title=Επιδιόρθωση
@@ -872,7 +863,7 @@ ocr.selectText.10=Λειτουργία OCR
ocr.selectText.11=Κατάργηση εικόνων μετά το OCR (Καταργεί ΟΛΕΣ τις εικόνες, είναι χρήσιμο μόνο αν αποτελεί μέρος του βήματος μετατροπής) ocr.selectText.11=Κατάργηση εικόνων μετά το OCR (Καταργεί ΟΛΕΣ τις εικόνες, είναι χρήσιμο μόνο αν αποτελεί μέρος του βήματος μετατροπής)
ocr.selectText.12=Τύπος απόδοσης (Για προχωρημένους) ocr.selectText.12=Τύπος απόδοσης (Για προχωρημένους)
ocr.help=Διαβάστε αυτήν την τεκμηρίωση σχετικά με τον τρόπο χρήσης αυτής για άλλες γλώσσες ή/και μη χρήσης σε docker ocr.help=Διαβάστε αυτήν την τεκμηρίωση σχετικά με τον τρόπο χρήσης αυτής για άλλες γλώσσες ή/και μη χρήσης σε docker
ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf και Tesseract για OCR. ocr.credit=Αυτή η υπηρεσία χρησιμοποιεί OCRmyPDF και Tesseract για OCR.
ocr.submit=Επεξεργασία PDF με OCR ocr.submit=Επεξεργασία PDF με OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Μετατροπή σε PDF
#compress #compress
compress.title=Συμπίεση compress.title=Συμπίεση
compress.header=Συμπίεση PDF compress.header=Συμπίεση PDF
compress.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF Συμπίεση/Βελτιστοποίηση. compress.credit=Αυτή η υπηρεσία χρησιμοποιεί Ghostscript για PDF Συμπίεση/Βελτιστοποίηση.
compress.selectText.1=Χειροκίνητη Λειτουργία - Από 1 έως 4 compress.selectText.1=Χειροκίνητη Λειτουργία - Από 1 έως 4
compress.selectText.2=Επίπεδο Βελτιστοποίησης: compress.selectText.2=Επίπεδο Βελτιστοποίησης:
compress.selectText.3=4 (Πολύ κακό για εικόνες κειμένου) compress.selectText.3=4 (Πολύ κακό για εικόνες κειμένου)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,13 +1035,12 @@ addPassword.submit=Κρυπτογράφηση
#watermark #watermark
watermark.title=Προσθήκη Υδατογραφήματος watermark.title=Προσθήκη Υδατογραφήματος
watermark.header=Προσθήκη Υδατογραφήματος watermark.header=Προσθήκη Υδατογραφήματος
watermark.customColor=Προσαρμοσμένο χρώμα κειμένου
watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος: watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος:
watermark.selectText.2=Κείμενο Υδατογραφήματος: watermark.selectText.2=Κείμενο Υδατογραφήματος:
watermark.selectText.3=Μέγεθος Κειμένου: watermark.selectText.3=Μέγεθος Κειμένου:
watermark.selectText.4=Περιστροφή (0-360): watermark.selectText.4=Περιστροφή (0-360):
watermark.selectText.5=Width Spacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια): watermark.selectText.5=widthSpacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
watermark.selectText.6=Height Spacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα): watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%): watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
watermark.selectText.8=Τύπος Υδατογραφήματος: watermark.selectText.8=Τύπος Υδατογραφήματος:
watermark.selectText.9=Εικόνα Υδατογραφήματος: watermark.selectText.9=Εικόνα Υδατογραφήματος:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Αλλαγή
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF σε PDF/A pdfToPDFA.title=PDF σε PDF/A
pdfToPDFA.header=PDF σε PDF/A pdfToPDFA.header=PDF σε PDF/A
pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί qpdf για PDF/A μετατροπή pdfToPDFA.credit=Αυτή η υπηρεσία χρησιμοποιεί ghostscript για PDF/A μετατροπή
pdfToPDFA.submit=Μετατροπή pdfToPDFA.submit=Μετατροπή
pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα pdfToPDFA.tip=Προς το παρόν δεν λειτουργεί για πολλαπλές εισόδους ταυτόχρονα
pdfToPDFA.outputFormat=Εξόδος αναμορφώσεων pdfToPDFA.outputFormat=Εξόδος αναμορφώσεων
@@ -1277,51 +1245,3 @@ splitByChapters.desc.2=Επίπεδο Σήμανσης Σκέψης: Επιλέ
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF. splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας. splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
splitByChapters.submit=Διαλύστε το PDF splitByChapters.submit=Διαλύστε το PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -73,7 +73,7 @@ joinDiscord=Join our Discord server
seeDockerHub=See Docker Hub seeDockerHub=See Docker Hub
visitGithub=Visit Github Repository visitGithub=Visit Github Repository
donate=Donate donate=Donate
color=Colour color=Color
sponsor=Sponsor sponsor=Sponsor
info=Info info=Info
pro=Pro 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 home.auto-rename.desc=Auto renames a PDF file based on its detected header
auto-rename.tags=auto-detect,header-based,organize,relabel 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 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.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!) 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.title=Split PDF by Sections
home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical 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.title=Add Stamp to PDF
home.AddStampRequest.desc=Add text or add image stamps at set locations 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 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. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Advanced Colour options 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.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 home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Colour,Page operations,Back end,server side replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert colour Options replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colours) replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customised colours) replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colours) replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast colour options replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Colour replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Colour replace-color.selectText.11=Choose background Color
replace-color.submit=Replace replace-color.submit=Replace
@@ -655,7 +651,7 @@ AddStampRequest.position=Position
AddStampRequest.overrideX=Override X Coordinate AddStampRequest.overrideX=Override X Coordinate
AddStampRequest.overrideY=Override Y Coordinate AddStampRequest.overrideY=Override Y Coordinate
AddStampRequest.customMargin=Custom Margin AddStampRequest.customMargin=Custom Margin
AddStampRequest.customColor=Custom Text Colour AddStampRequest.customColor=Custom Text Color
AddStampRequest.submit=Submit AddStampRequest.submit=Submit
@@ -787,8 +783,8 @@ removeAnnotations.submit=Remove
#compare #compare
compare.title=Compare compare.title=Compare
compare.header=Compare PDFs compare.header=Compare PDFs
compare.highlightColor.1=Highlight Colour 1: compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Colour 2: compare.highlightColor.2=Highlight Color 2:
compare.document.1=Document 1 compare.document.1=Document 1
compare.document.2=Document 2 compare.document.2=Document 2
compare.submit=Compare compare.submit=Compare
@@ -822,12 +818,7 @@ sign.save=Save Signature
sign.personalSigs=Personal Signatures sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Repair repair.title=Repair
@@ -846,7 +837,7 @@ flatten.submit=Flatten
ScannerImageSplit.selectText.1=Angle Threshold: ScannerImageSplit.selectText.1=Angle Threshold:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10). ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
ScannerImageSplit.selectText.3=Tolerance: 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.5=Minimum Area:
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000). ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000).
ScannerImageSplit.selectText.7=Minimum Contour Area: 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.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
ocr.selectText.12=Render Type (Advanced) 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.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 ocr.submit=Process PDF with OCR
@@ -896,9 +887,9 @@ fileToPDF.submit=Convert to PDF
#compress #compress
compress.title=Compress compress.title=Compress
compress.header=Compress PDF 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.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.3=4 (Terrible for text images)
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size 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) compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,13 +1035,12 @@ addPassword.submit=Encrypt
#watermark #watermark
watermark.title=Add Watermark watermark.title=Add Watermark
watermark.header=Add Watermark watermark.header=Add Watermark
watermark.customColor=Custom Text Colour
watermark.selectText.1=Select PDF to add watermark to: watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text: watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size: watermark.selectText.3=Font Size:
watermark.selectText.4=Rotation (0-360): watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=Width Spacer (Space between each watermark horizontally): watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
watermark.selectText.6=Height Spacer (Space between each watermark vertically): watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Change
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF To PDF/A pdfToPDFA.title=PDF To PDF/A
pdfToPDFA.header=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.submit=Convert
pdfToPDFA.tip=Currently does not work for multiple inputs at once pdfToPDFA.tip=Currently does not work for multiple inputs at once
pdfToPDFA.outputFormat=Output format pdfToPDFA.outputFormat=Output format
@@ -1277,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.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.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF replace-color.header=Replace-Invert Color PDF
@@ -822,12 +818,7 @@ sign.save=Save Signature
sign.personalSigs=Personal Signatures sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Repair repair.title=Repair
@@ -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.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
ocr.selectText.12=Render Type (Advanced) 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.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 ocr.submit=Process PDF with OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Convert to PDF
#compress #compress
compress.title=Compress compress.title=Compress
compress.header=Compress PDF 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.1=Manual Mode - From 1 to 4
compress.selectText.2=Optimization level: compress.selectText.2=Optimization level:
compress.selectText.3=4 (Terrible for text images) compress.selectText.3=4 (Terrible for text images)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,13 +1035,12 @@ addPassword.submit=Encrypt
#watermark #watermark
watermark.title=Add Watermark watermark.title=Add Watermark
watermark.header=Add Watermark watermark.header=Add Watermark
watermark.customColor=Custom Text Color
watermark.selectText.1=Select PDF to add watermark to: watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text: watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size: watermark.selectText.3=Font Size:
watermark.selectText.4=Rotation (0-360): watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=Width Spacer (Space between each watermark horizontally): watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
watermark.selectText.6=Height Spacer (Space between each watermark vertically): watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Change
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF To PDF/A pdfToPDFA.title=PDF To PDF/A
pdfToPDFA.header=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.submit=Convert
pdfToPDFA.tip=Currently does not work for multiple inputs at once pdfToPDFA.tip=Currently does not work for multiple inputs at once
pdfToPDFA.outputFormat=Output format pdfToPDFA.outputFormat=Output format
@@ -1277,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.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.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF 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

View File

@@ -512,10 +512,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. home.splitPdfByChapters.desc=Divida un PDF en varios archivos según su estructura de capítulos.
splitPdfByChapters.tags=dividir,capítulos,marcadores,organizar 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-invert-color
replace-color.title=Reemplazar-Invertir-Color replace-color.title=Reemplazar-Invertir-Color
replace-color.header=Reemplazar-Invertir Color en PDF replace-color.header=Reemplazar-Invertir Color en PDF
@@ -822,12 +818,7 @@ sign.save=Guardar Firma
sign.personalSigs=Firmas Personales sign.personalSigs=Firmas Personales
sign.sharedSigs=Firmas compartidas sign.sharedSigs=Firmas compartidas
sign.noSavedSigs=No se encontraron firmas guardadas 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
repair.title=Reparar repair.title=Reparar
@@ -872,7 +863,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.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.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.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 ocr.submit=Procesar PDF con OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Convertir a PDF
#compress #compress
compress.title=Comprimir compress.title=Comprimir
compress.header=Comprimir PDF 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.1=Modo manual - De 1 a 4
compress.selectText.2=Nivel de optimización: compress.selectText.2=Nivel de optimización:
compress.selectText.3=4 (Terrible para imágenes de texto) compress.selectText.3=4 (Terrible para imágenes de texto)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Encriptar
#watermark #watermark
watermark.title=Añadir marca de agua watermark.title=Añadir marca de agua
watermark.header=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.1=Seleccionar PDF para añadir marca de agua:
watermark.selectText.2=Texto de la marca de agua: watermark.selectText.2=Texto de la marca de agua:
watermark.selectText.3=Tamaño de la Fuente: watermark.selectText.3=Tamaño de la Fuente:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Cambiar
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF a PDF/A pdfToPDFA.title=PDF a PDF/A
pdfToPDFA.header=PDF a PDF/A pdfToPDFA.header=PDF a PDF/A
pdfToPDFA.credit=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.submit=Convertir
pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez pdfToPDFA.tip=Actualmente no funciona para múltiples entrada a la vez
pdfToPDFA.outputFormat=Formato de salida pdfToPDFA.outputFormat=Formato de salida
@@ -1277,51 +1245,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.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.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 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF replace-color.header=Replace-Invert Color PDF
@@ -822,12 +818,7 @@ sign.save=Save Signature
sign.personalSigs=Personal Signatures sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Konpondu repair.title=Konpondu
@@ -872,7 +863,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.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.selectText.12=Prozesaketa-mota (aurreratua)
ocr.help=Irakurri honen erabilerari buruzko dokumentazioa beste hizkuntza batzuetarako eta/edo ez erabili Docker-en 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 ocr.submit=PDF prozesatu OCR-rekin
@@ -896,7 +887,7 @@ fileToPDF.submit=PDF bihurtu
#compress #compress
compress.title=Konprimatu compress.title=Konprimatu
compress.header=PDFa 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.1=Eskuz 1etik 4ra
compress.selectText.2=Optimizazio maila: compress.selectText.2=Optimizazio maila:
compress.selectText.3=4 (Izugarria testu-irudietarako) compress.selectText.3=4 (Izugarria testu-irudietarako)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Enkriptatu
#watermark #watermark
watermark.title=Gehitu ur-marka watermark.title=Gehitu ur-marka
watermark.header=Gehitu ur-marka watermark.header=Gehitu ur-marka
watermark.customColor=Custom Text Color
watermark.selectText.1=Hautatu PDFa ur-marka gehitzeko: watermark.selectText.1=Hautatu PDFa ur-marka gehitzeko:
watermark.selectText.2=Ur-markaren testua: watermark.selectText.2=Ur-markaren testua:
watermark.selectText.3=Letra-tipoaren tamaina: watermark.selectText.3=Letra-tipoaren tamaina:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Aldatu
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDFa PDF/A bihurtu pdfToPDFA.title=PDFa PDF/A bihurtu
pdfToPDFA.header=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.submit=Bihurtu
pdfToPDFA.tip=Currently does not work for multiple inputs at once pdfToPDFA.tip=Currently does not work for multiple inputs at once
pdfToPDFA.outputFormat=Output format pdfToPDFA.outputFormat=Output format
@@ -1277,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.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.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF 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

View File

@@ -56,8 +56,8 @@ userNotFoundMessage=Utilisateur non trouvé.
incorrectPasswordMessage=Le mot de passe actuel est incorrect. incorrectPasswordMessage=Le mot de passe actuel est incorrect.
usernameExistsMessage=Le nouveau nom d'utilisateur existe déjà. 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. 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. invalidPasswordMessage=Le mot de passe ne peut pas être vide et ne doit pas contenir d'espaces au début ou en fin.
confirmPasswordErrorMessage=Le nouveau mot de passe et sa confirmation doivent être identiques. confirmPasswordErrorMessage=Nouveau Mot de passe et Confirmer le Nouveau Mot de passe doivent correspondre.
deleteCurrentUserMessage=Impossible de supprimer l'utilisateur actuellement connecté. deleteCurrentUserMessage=Impossible de supprimer l'utilisateur actuellement connecté.
deleteUsernameExistsMessage=Le nom d'utilisateur n'existe pas et ne peut pas être supprimé. 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. downgradeCurrentUserMessage=Impossible de rétrograder le rôle de l'utilisateur actuel.
@@ -81,7 +81,7 @@ page=Page
pages=Pages pages=Pages
loading=Chargement... loading=Chargement...
addToDoc=Ajouter au Document addToDoc=Ajouter au Document
reset=Réinitialiser reset=Reset
legal.privacy=Politique de Confidentialité legal.privacy=Politique de Confidentialité
legal.terms=Conditions Générales legal.terms=Conditions Générales
@@ -142,7 +142,7 @@ navbar.language=Langues
navbar.settings=Paramètres navbar.settings=Paramètres
navbar.allTools=Outils navbar.allTools=Outils
navbar.multiTool=Outils Multiples navbar.multiTool=Outils Multiples
navbar.search=Rechercher navbar.search=Search
navbar.sections.organize=Organisation navbar.sections.organize=Organisation
navbar.sections.convertTo=Convertir en PDF navbar.sections.convertTo=Convertir en PDF
navbar.sections.convertFrom=Convertir depuis PDF navbar.sections.convertFrom=Convertir depuis PDF
@@ -512,10 +512,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. 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 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-invert-color
replace-color.title=Remplacer-Inverser-Couleur replace-color.title=Remplacer-Inverser-Couleur
replace-color.header=Remplacer-Inverser Couleur PDF replace-color.header=Remplacer-Inverser Couleur PDF
@@ -817,17 +813,12 @@ sign.draw=Dessiner une signature
sign.text=Saisir de texte sign.text=Saisir de texte
sign.clear=Effacer sign.clear=Effacer
sign.add=Ajouter sign.add=Ajouter
sign.saved=Sceaux enregistrées sign.saved=Saved Signatures
sign.save=Enregistrer le sceau sign.save=Enregistrer le sceau
sign.personalSigs=Sceaux personnels sign.personalSigs=Sceaux personnels
sign.sharedSigs=Sceaux partagés sign.sharedSigs=Sceaux partagés
sign.noSavedSigs=Aucun sceau enregistré trouvé 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
repair.title=Réparer repair.title=Réparer
@@ -872,7 +863,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.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.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.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 ocr.submit=Traiter
@@ -896,7 +887,7 @@ fileToPDF.submit=Convertir
#compress #compress
compress.title=Compresser un PDF compress.title=Compresser un PDF
compress.header=Compresser un PDF (lorsque c'est possible!) 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.1=Mode manuel de 1 à 4
compress.selectText.2=Niveau d'optimisation compress.selectText.2=Niveau d'optimisation
compress.selectText.3=4 (terrible pour les images textuelles) compress.selectText.3=4 (terrible pour les images textuelles)
@@ -944,39 +935,17 @@ pdfOrganiser.placeholder=(par exemple 1,3,2 ou 4-8,2,10-12 ou 2n-1)
multiTool.title=Outil multifonction PDF multiTool.title=Outil multifonction PDF
multiTool.header=Outil multifonction PDF multiTool.header=Outil multifonction PDF
multiTool.uploadPrompts=Nom du fichier multiTool.uploadPrompts=Nom du fichier
multiTool.selectAll=Tout sélectionner multiTool.selectAll=Select All
multiTool.deselectAll=Tout déselectionner multiTool.deselectAll=Deselect All
multiTool.selectPages=Sélection des pages multiTool.selectPages=Page Select
multiTool.selectedPages=Pages sélectionnées multiTool.selectedPages=Selected Pages
multiTool.page=Page multiTool.page=Page
multiTool.deleteSelected=Supprimer la sélection multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Exporter multiTool.downloadAll=Export
multiTool.downloadSelected=Exporter la sélection multiTool.downloadSelected=Export Selected
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
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=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 #view pdf
viewPdf.title=Visualiser un PDF viewPdf.title=Visualiser un PDF
@@ -1066,13 +1035,12 @@ addPassword.submit=Chiffrer
#watermark #watermark
watermark.title=Ajouter un filigrane watermark.title=Ajouter un filigrane
watermark.header=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.1=PDF auquel ajouter un filigrane
watermark.selectText.2=Texte du filigrane watermark.selectText.2=Texte du filigrane
watermark.selectText.3=Taille de police watermark.selectText.3=Taille de police
watermark.selectText.4=Rotation (de 0 à 360 degrés) watermark.selectText.4=Rotation (de 0 à 360 degrés)
watermark.selectText.5=Width Spacer (espace entre chaque filigrane horizontalement) watermark.selectText.5=widthSpacer (espace entre chaque filigrane horizontalement)
watermark.selectText.6=Height Spacer (espace entre chaque filigrane verticalement) watermark.selectText.6=heightSpacer (espace entre chaque filigrane verticalement)
watermark.selectText.7=Opacité (de 0% à 100%) watermark.selectText.7=Opacité (de 0% à 100%)
watermark.selectText.8=Type de filigrane watermark.selectText.8=Type de filigrane
watermark.selectText.9=Image du filigrane watermark.selectText.9=Image du filigrane
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Modifier
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF en PDF/A pdfToPDFA.title=PDF en PDF/A
pdfToPDFA.header=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.submit=Convertir
pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois
pdfToPDFA.outputFormat=Format de sortie pdfToPDFA.outputFormat=Format de sortie
@@ -1277,51 +1245,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.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.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 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure. home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF replace-color.header=Replace-Invert Color PDF
@@ -822,12 +818,7 @@ sign.save=Save Signature
sign.personalSigs=Personal Signatures sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=Deisiúchán repair.title=Deisiúchán
@@ -872,7 +863,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.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.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.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 ocr.submit=Próiseáil PDF le OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Tiontaigh go PDF
#compress #compress
compress.title=Comhbhrúigh compress.title=Comhbhrúigh
compress.header=Comhbhrúigh PDF 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.1=Mód Láimhe - Ó 1 go 4
compress.selectText.2=Leibhéal optamaithe: compress.selectText.2=Leibhéal optamaithe:
compress.selectText.3=4 (Uafásach le haghaidh íomhánna téacs) compress.selectText.3=4 (Uafásach le haghaidh íomhánna téacs)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,12 +1035,11 @@ addPassword.submit=Criptigh
#watermark #watermark
watermark.title=Cuir Uisce leis watermark.title=Cuir Uisce leis
watermark.header=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.1=Roghnaigh PDF chun comhartha uisce a chur leis:
watermark.selectText.2=Téacs Comhartha Uisce: watermark.selectText.2=Téacs Comhartha Uisce:
watermark.selectText.3=Méid cló: watermark.selectText.3=Méid cló:
watermark.selectText.4=Rothlú (0-360): 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.6=spásaire airde (Spás idir gach comhartha uisce go hingearach):
watermark.selectText.7=Teimhneacht (0% - 100%): watermark.selectText.7=Teimhneacht (0% - 100%):
watermark.selectText.8=Cineál Comhartha Uisce: watermark.selectText.8=Cineál Comhartha Uisce:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Athrú
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF Go PDF/A pdfToPDFA.title=PDF Go PDF/A
pdfToPDFA.header=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.submit=Tiontaigh
pdfToPDFA.tip=Faoi láthair ní oibríonn sé le haghaidh ionchuir iolracha ag an am céanna pdfToPDFA.tip=Faoi láthair ní oibríonn sé le haghaidh ionchuir iolracha ag an am céanna
pdfToPDFA.outputFormat=Formáid aschuir pdfToPDFA.outputFormat=Formáid aschuir
@@ -1277,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.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.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=अध्यायों पर अलग-कर
home.splitPdfByChapters.desc=पुस्तक के अध्याय की संरचना पर आधारित एक PDF को बहिन-भागों में विभाजित करें home.splitPdfByChapters.desc=पुस्तक के अध्याय की संरचना पर आधारित एक PDF को बहिन-भागों में विभाजित करें
splitPdfByChapters.tags=विभाजन,अध्याय,पसंदीदा,रजैत splitPdfByChapters.tags=विभाजन,अध्याय,पसंदीदा,रजैत
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color #replace-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=चित्र रंग परिवर्तन/उलटकर परिवर्तन PDF replace-color.header=चित्र रंग परिवर्तन/उलटकर परिवर्तन PDF
@@ -822,12 +818,7 @@ sign.save=प्रदर्शन बचाएं
sign.personalSigs=मौजूदा प्रदर्शन sign.personalSigs=मौजूदा प्रदर्शन
sign.sharedSigs=साझेदार प्रदर्शन sign.sharedSigs=साझेदार प्रदर्शन
sign.noSavedSigs=कोई भी संवर्तित प्रदर्शन नहीं मौजूद है sign.noSavedSigs=कोई भी संवर्तित प्रदर्शन नहीं मौजूद है
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
#repair #repair
repair.title=मरम्मत repair.title=मरम्मत
@@ -872,7 +863,7 @@ ocr.selectText.10=OCR मोड
ocr.selectText.11=OCR के बाद छवियां हटाएँ (सभी छवियां हटाएँ, केवल परिवर्तन चरण का हिस्सा होता है) ocr.selectText.11=OCR के बाद छवियां हटाएँ (सभी छवियां हटाएँ, केवल परिवर्तन चरण का हिस्सा होता है)
ocr.selectText.12=रेंडर टाइप (उन्नत) ocr.selectText.12=रेंडर टाइप (उन्नत)
ocr.help=कृपया इस डॉक्यूमेंटेशन को पढ़ें कि इसे अन्य भाषाओं के लिए कैसे उपयोग किया जाता है और/या डॉकर में नहीं हैं ocr.help=कृपया इस डॉक्यूमेंटेशन को पढ़ें कि इसे अन्य भाषाओं के लिए कैसे उपयोग किया जाता है और/या डॉकर में नहीं हैं
ocr.credit=इस सेवा में qpdf और टेसरेक्ट का उपयोग होता है। ocr.credit=इस सेवा में OCRmyPDF और टेसरेक्ट का उपयोग होता है।
ocr.submit=OCR के साथ PDF प्रोसेस करें ocr.submit=OCR के साथ PDF प्रोसेस करें
@@ -896,7 +887,7 @@ fileToPDF.submit=पीडीएफ़ में बदलें
#compress #compress
compress.title=संकुचित करें compress.title=संकुचित करें
compress.header=PDF को संकुचित करें compress.header=PDF को संकुचित करें
compress.credit=यह सेवा PDF संकुचन/अनुकूलन के लिए qpdf का उपयोग करती है। compress.credit=यह सेवा PDF संकुचन/अनुकूलन के लिए Ghostscript का उपयोग करती है।
compress.selectText.1=मैनुअल मोड - 1 से 4 तक compress.selectText.1=मैनुअल मोड - 1 से 4 तक
compress.selectText.2=अनुकूलन स्तर: compress.selectText.2=अनुकूलन स्तर:
compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक) compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=एन्क्रिप्ट करें
#watermark #watermark
watermark.title=वॉटरमार्क जोड़ें watermark.title=वॉटरमार्क जोड़ें
watermark.header=वॉटरमार्क जोड़ें watermark.header=वॉटरमार्क जोड़ें
watermark.customColor=संवैधित टेक्स्ट रंग
watermark.selectText.1=वॉटरमार्क जोड़ने के लिए पीडीएफ चुनें: watermark.selectText.1=वॉटरमार्क जोड़ने के लिए पीडीएफ चुनें:
watermark.selectText.2=वॉटरमार्क टेक्स्ट: watermark.selectText.2=वॉटरमार्क टेक्स्ट:
watermark.selectText.3=फ़ॉन्ट साइज़: watermark.selectText.3=फ़ॉन्ट साइज़:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=बदलें
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF से PDF/A में pdfToPDFA.title=PDF से PDF/A में
pdfToPDFA.header=PDF से PDF/A में pdfToPDFA.header=PDF से PDF/A में
pdfToPDFA.credit=इस सेवा में PDF/A परिवर्तन के लिए qpdf का उपयोग किया जाता है। pdfToPDFA.credit=इस सेवा में PDF/A परिवर्तन के लिए ghostscript का उपयोग किया जाता है।
pdfToPDFA.submit=परिवर्तित करें pdfToPDFA.submit=परिवर्तित करें
pdfToPDFA.tip=यह सैकड़ों प्रविष्टियाँ एक ही समय में काम करते हैं pdfToPDFA.tip=यह सैकड़ों प्रविष्टियाँ एक ही समय में काम करते हैं
pdfToPDFA.outputFormat=आउटपुट फॉर्मेट pdfToPDFA.outputFormat=आउटपुट फॉर्मेट
@@ -1277,51 +1245,3 @@ splitByChapters.desc.2=लेमैक्स स्तर: विभाजन
splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा। splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा।
splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है। splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है।
splitByChapters.submit=PDF विभाजित splitByChapters.submit=PDF विभाजित
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Podijeli PDF prema glavama
home.splitPdfByChapters.desc=Podijeli PDF na više datoteka prema njegovom strukturnom obliku glava. home.splitPdfByChapters.desc=Podijeli PDF na više datoteka prema njegovom strukturnom obliku glava.
splitPdfByChapters.tags=podjela, glave, markere, organizacija 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-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Zameni-inverziranje boja u PDF-u replace-color.header=Zameni-inverziranje boja u PDF-u
@@ -822,12 +818,7 @@ sign.save=Sačuvaj potpisnu oznaku
sign.personalSigs=Osobni potpisi sign.personalSigs=Osobni potpisi
sign.sharedSigs=Dijeljeni potpisi sign.sharedSigs=Dijeljeni potpisi
sign.noSavedSigs=Nema sacuvanih potpisa pronađenih 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
repair.title=Popravi repair.title=Popravi
@@ -872,7 +863,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.11=Ukloni slike nakon OCR-a (Uklanja SVE slike, korisno samo ako je dio koraka konverzije)
ocr.selectText.12=Vrsta iscrtavanja (napredno) 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.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 ocr.submit=Obradi PDF sa OCR-om
@@ -896,7 +887,7 @@ fileToPDF.submit=Pretvori u PDF
#compress #compress
compress.title=Komprimirajte compress.title=Komprimirajte
compress.header=Komprimirajte PDF 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.1=Ručni režim - Od 1 do 4
compress.selectText.2=Nivo optimizacije: compress.selectText.2=Nivo optimizacije:
compress.selectText.3=4 (Užasno za tekstualne slike) compress.selectText.3=4 (Užasno za tekstualne slike)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,7 +1035,6 @@ addPassword.submit=Šifriraj
#watermark #watermark
watermark.title=Dodaj vodeni žig watermark.title=Dodaj vodeni žig
watermark.header=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.1=Izaberite PDF za dodavanje vodenog žiga:
watermark.selectText.2=Tekst vodenog žiga: watermark.selectText.2=Tekst vodenog žiga:
watermark.selectText.3=Veličina fonta: watermark.selectText.3=Veličina fonta:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Promijeniti
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF u PDF/A pdfToPDFA.title=PDF u PDF/A
pdfToPDFA.header=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.submit=Pretvoriti
pdfToPDFA.tip=Trenutno ne radi za više unosa odjednom pdfToPDFA.tip=Trenutno ne radi za više unosa odjednom
pdfToPDFA.outputFormat=Izlazni format pdfToPDFA.outputFormat=Izlazni format
@@ -1277,51 +1245,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.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.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 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=PDF felosztása fejezetek szerint
home.splitPdfByChapters.desc=Fejezetei alapján egy PDF fájl több dokumentumba osztás. home.splitPdfByChapters.desc=Fejezetei alapján egy PDF fájl több dokumentumba osztás.
splitPdfByChapters.tags=Osztás, fejezetek, jelezes, organizálá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-invert-color
replace-color.title=Replace-Invert-Color replace-color.title=Replace-Invert-Color
replace-color.header=Visszaalakítás-összevétel a színekkel PDF-ben replace-color.header=Visszaalakítás-összevétel a színekkel PDF-ben
@@ -822,12 +818,7 @@ sign.save=Aláíráshoz mentés
sign.personalSigs=Személyi aláíráshoz sign.personalSigs=Személyi aláíráshoz
sign.sharedSigs=Megosztott aláíráshoz sign.sharedSigs=Megosztott aláíráshoz
sign.noSavedSigs=Nincsenek mentett aláírások találat 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
repair.title=Javítás repair.title=Javítás
@@ -872,7 +863,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.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.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.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 ocr.submit=PDF feldolgozása OCR-rel
@@ -896,7 +887,7 @@ fileToPDF.submit=Konvertálás PDF dokumentummá
#compress #compress
compress.title=Tömörítés compress.title=Tömörítés
compress.header=PDF tömörítése 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.1=Kézi mód - 1-től 4-ig
compress.selectText.2=Optimalizálási szint: compress.selectText.2=Optimalizálási szint:
compress.selectText.3=4 (nem ajánlott a szöveges képekhez) compress.selectText.3=4 (nem ajánlott a szöveges képekhez)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,13 +1035,12 @@ addPassword.submit=Titkosítás
#watermark #watermark
watermark.title=Vízjel hozzáadása watermark.title=Vízjel hozzáadása
watermark.header=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.1=Válassza ki a PDF-t, amelyhez vízjelet kíván hozzáadni:
watermark.selectText.2=Vízjel szövege: watermark.selectText.2=Vízjel szövege:
watermark.selectText.3=Betűméret: watermark.selectText.3=Betűméret:
watermark.selectText.4=Forgatás (0-360): watermark.selectText.4=Forgatás (0-360):
watermark.selectText.5=Width Spacer (Hely a vízjelek között vízszintesen): watermark.selectText.5=widthSpacer (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.6=heightSpacer (Hely a vízjelek között függőlegesen):
watermark.selectText.7=Átlátszóság (0% - 100%): watermark.selectText.7=Átlátszóság (0% - 100%):
watermark.selectText.8=Vízjel típusa: watermark.selectText.8=Vízjel típusa:
watermark.selectText.9=Vízjel képe: watermark.selectText.9=Vízjel képe:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Módosítás
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF >> PDF/A pdfToPDFA.title=PDF >> PDF/A
pdfToPDFA.header=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.submit=Konvertálás
pdfToPDFA.tip=Jelenleg egyszerre több fájl nem működik ezzel a funkcióval pdfToPDFA.tip=Jelenleg egyszerre több fájl nem működik ezzel a funkcióval
pdfToPDFA.outputFormat=Kimeneti formátum pdfToPDFA.outputFormat=Kimeneti formátum
@@ -1277,51 +1245,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.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.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 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

View File

@@ -512,10 +512,6 @@ home.splitPdfByChapters.title=Pisahkan PDF berdasarkan Bab
home.splitPdfByChapters.desc=Memisahkan PDF menjadi beberapa file berdasarkan struktur babnya. home.splitPdfByChapters.desc=Memisahkan PDF menjadi beberapa file berdasarkan struktur babnya.
splitPdfByChapters.tags=pemisahan,bab,bookmark,atur 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-invert-color
replace-color.title=Ganti-Inversi-Warna replace-color.title=Ganti-Inversi-Warna
replace-color.header=Ganti-Inversi Warna PDF replace-color.header=Ganti-Inversi Warna PDF
@@ -822,12 +818,7 @@ sign.save=Simpan Tanda Tangan
sign.personalSigs=Tanda Tangan Pribadi sign.personalSigs=Tanda Tangan Pribadi
sign.sharedSigs=Tanda Tangan Berbagi sign.sharedSigs=Tanda Tangan Berbagi
sign.noSavedSigs=Tidak ditemukan tanda tangan yang disimpan 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
repair.title=Perbaiki repair.title=Perbaiki
@@ -872,7 +863,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.11=Hapus gambar setelah OCR (Menghapus Semua gambar, hanya berguna jika merupakan bagian dari langkah konversi)
ocr.selectText.12=Jenis Render (Lanjutan) 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.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 ocr.submit=Memproses PDF dengan OCR
@@ -896,7 +887,7 @@ fileToPDF.submit=Konversi ke PDF
#compress #compress
compress.title=Kompres compress.title=Kompres
compress.header=Kompres PDF 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.1=Mode Manual - Dari 1 hingga 4
compress.selectText.2=Tingkat Optimalisasi: compress.selectText.2=Tingkat Optimalisasi:
compress.selectText.3=4 (Buruk untuk gambar teks) compress.selectText.3=4 (Buruk untuk gambar teks)
@@ -953,28 +944,6 @@ multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Export Selected
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1066,13 +1035,12 @@ addPassword.submit=Enkripsi
#watermark #watermark
watermark.title=Tambahkan Watermark watermark.title=Tambahkan Watermark
watermark.header=Tambahkan Watermark watermark.header=Tambahkan Watermark
watermark.customColor=Warna Teks Kustom
watermark.selectText.1=Pilih PDF untuk menambahkan watermark: watermark.selectText.1=Pilih PDF untuk menambahkan watermark:
watermark.selectText.2=Text Watermark: watermark.selectText.2=Text Watermark:
watermark.selectText.3=Ukuran Huruf: watermark.selectText.3=Ukuran Huruf:
watermark.selectText.4=Rotasi (0-360): watermark.selectText.4=Rotasi (0-360):
watermark.selectText.5=Width Spacer (Spasi diantara setiap watermark horisontal): watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal):
watermark.selectText.6=Height Spacer (Spasi diantara setiap watermark vertikal): watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal):
watermark.selectText.7=Kejernihan (0% - 100%): watermark.selectText.7=Kejernihan (0% - 100%):
watermark.selectText.8=Tipe Watermark: watermark.selectText.8=Tipe Watermark:
watermark.selectText.9=Gambar Watermark: watermark.selectText.9=Gambar Watermark:
@@ -1129,7 +1097,7 @@ changeMetadata.submit=Ganti
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF Ke PDF/A pdfToPDFA.title=PDF Ke PDF/A
pdfToPDFA.header=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.submit=Konversi
pdfToPDFA.tip=Saat ini tidak dapat digunakan untuk beberapa input sekaligus pdfToPDFA.tip=Saat ini tidak dapat digunakan untuk beberapa input sekaligus
pdfToPDFA.outputFormat=Format keluaran pdfToPDFA.outputFormat=Format keluaran
@@ -1277,51 +1245,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.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.desc.4=Izinkan Duplikat: Jika dicentang, mengizinkan beberapa markah pada halaman yang sama untuk membuat PDF terpisah.
splitByChapters.submit=Pecah PDF 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

Some files were not shown because too many files have changed in this diff Show More