mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 21:30:14 +03:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b044c0dc98 | ||
|
|
0c32dd53b2 | ||
|
|
f4b2f2d5b8 | ||
|
|
5d77373ef8 | ||
|
|
3dd4a33595 | ||
|
|
fe2bfd8739 | ||
|
|
a15b0e33d5 | ||
|
|
0a242f6a57 | ||
|
|
3c46010155 | ||
|
|
d854497266 | ||
|
|
57ab30d1a6 | ||
|
|
7dbf529a45 | ||
|
|
02189a67bd | ||
|
|
d4985f57d4 | ||
|
|
4ab66fdf14 | ||
|
|
c19abe0da7 | ||
|
|
dd6b7968db | ||
|
|
2228ae7197 | ||
|
|
30987dcad2 | ||
|
|
43beadbdcb | ||
|
|
416d79aed3 | ||
|
|
abc0988fdf | ||
|
|
c7e0ea5b5b | ||
|
|
b35447934e | ||
|
|
d1e82eb8f1 | ||
|
|
7d44cc1a40 |
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Author: Ludy87
|
||||
Description: This script processes JSON translation files for localization checks. It compares translation files in a branch with
|
||||
a reference file to ensure consistency. The script performs two main checks:
|
||||
1. Verifies that the number of translation keys in the translation files matches the reference file.
|
||||
2. Ensures that all keys in the translation files are present in the reference file and vice versa.
|
||||
|
||||
The script also provides functionality to update the translation files to match the reference file by adding missing keys and
|
||||
adjusting the format.
|
||||
|
||||
Usage:
|
||||
python check_language_json.py --reference-file <path_to_reference_file> --branch <branch_name> [--actor <actor_name>] [--files <list_of_changed_files>]
|
||||
"""
|
||||
# Sample for Windows:
|
||||
# python .github/scripts/check_language_json.py --reference-file frontend/public/locales/en-GB/translation.json --branch "" --files frontend/public/locales/de-DE/translation.json frontend/public/locales/fr-FR/translation.json
|
||||
|
||||
import copy
|
||||
import glob
|
||||
import os
|
||||
import argparse
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
def find_duplicate_keys(file_path, keys=None, prefix=""):
|
||||
"""
|
||||
Identifies duplicate keys in a JSON file (including nested keys).
|
||||
:param file_path: Path to the JSON file.
|
||||
:param keys: Dictionary to track keys (used for recursion).
|
||||
:param prefix: Prefix for nested keys.
|
||||
:return: List of tuples (key, first_occurrence_path, duplicate_path).
|
||||
"""
|
||||
if keys is None:
|
||||
keys = {}
|
||||
|
||||
duplicates = []
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def process_dict(obj, current_prefix=""):
|
||||
for key, value in obj.items():
|
||||
full_key = f"{current_prefix}.{key}" if current_prefix else key
|
||||
|
||||
if isinstance(value, dict):
|
||||
process_dict(value, full_key)
|
||||
else:
|
||||
if full_key in keys:
|
||||
duplicates.append((full_key, keys[full_key], full_key))
|
||||
else:
|
||||
keys[full_key] = full_key
|
||||
|
||||
process_dict(data, prefix)
|
||||
return duplicates
|
||||
|
||||
|
||||
# Maximum size for JSON files (e.g., 500 KB)
|
||||
MAX_FILE_SIZE = 500 * 1024
|
||||
|
||||
|
||||
def parse_json_file(file_path):
|
||||
"""
|
||||
Parses a JSON translation file and returns a flat dictionary of all keys.
|
||||
:param file_path: Path to the JSON file.
|
||||
:return: Dictionary with flattened keys.
|
||||
"""
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
data = json.load(file)
|
||||
|
||||
def flatten_dict(d, parent_key="", sep="."):
|
||||
items = {}
|
||||
for k, v in d.items():
|
||||
new_key = f"{parent_key}{sep}{k}" if parent_key else k
|
||||
if isinstance(v, dict):
|
||||
items.update(flatten_dict(v, new_key, sep=sep))
|
||||
else:
|
||||
items[new_key] = v
|
||||
return items
|
||||
|
||||
return flatten_dict(data)
|
||||
|
||||
|
||||
def unflatten_dict(d, sep="."):
|
||||
"""
|
||||
Converts a flat dictionary with dot notation keys back to nested dict.
|
||||
:param d: Flattened dictionary.
|
||||
:param sep: Separator used in keys.
|
||||
:return: Nested dictionary.
|
||||
"""
|
||||
result = {}
|
||||
for key, value in d.items():
|
||||
parts = key.split(sep)
|
||||
current = result
|
||||
for part in parts[:-1]:
|
||||
if part not in current:
|
||||
current[part] = {}
|
||||
current = current[part]
|
||||
current[parts[-1]] = value
|
||||
return result
|
||||
|
||||
|
||||
def write_json_file(file_path, updated_properties):
|
||||
"""
|
||||
Writes updated properties back to the JSON file.
|
||||
:param file_path: Path to the JSON file.
|
||||
:param updated_properties: Dictionary of updated properties to write.
|
||||
"""
|
||||
nested_data = unflatten_dict(updated_properties)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8", newline="\n") as file:
|
||||
json.dump(nested_data, file, ensure_ascii=False, indent=2)
|
||||
file.write("\n") # Add trailing newline
|
||||
|
||||
|
||||
def update_missing_keys(reference_file, file_list, branch=""):
|
||||
"""
|
||||
Updates missing keys in the translation files based on the reference file.
|
||||
:param reference_file: Path to the reference JSON file.
|
||||
:param file_list: List of translation files to update.
|
||||
:param branch: Branch where the files are located.
|
||||
"""
|
||||
reference_properties = parse_json_file(reference_file)
|
||||
|
||||
for file_path in file_list:
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_path))
|
||||
if (
|
||||
basename_current_file == os.path.basename(reference_file)
|
||||
or not file_path.endswith(".json")
|
||||
or not os.path.dirname(file_path).endswith("locales")
|
||||
):
|
||||
continue
|
||||
|
||||
current_properties = parse_json_file(os.path.join(branch, file_path))
|
||||
updated_properties = {}
|
||||
|
||||
for ref_key, ref_value in reference_properties.items():
|
||||
if ref_key in current_properties:
|
||||
# Keep the current translation
|
||||
updated_properties[ref_key] = current_properties[ref_key]
|
||||
else:
|
||||
# Add missing key with reference value
|
||||
updated_properties[ref_key] = ref_value
|
||||
|
||||
write_json_file(os.path.join(branch, file_path), updated_properties)
|
||||
|
||||
|
||||
def check_for_missing_keys(reference_file, file_list, branch):
|
||||
update_missing_keys(reference_file, file_list, branch)
|
||||
|
||||
|
||||
def read_json_keys(file_path):
|
||||
if os.path.isfile(file_path) and os.path.exists(file_path):
|
||||
return parse_json_file(file_path)
|
||||
return {}
|
||||
|
||||
|
||||
def check_for_differences(reference_file, file_list, branch, actor):
|
||||
reference_branch = branch
|
||||
basename_reference_file = os.path.basename(reference_file)
|
||||
|
||||
report = []
|
||||
report.append(f"#### 🔄 Reference Branch: `{reference_branch}`")
|
||||
reference_keys = read_json_keys(reference_file)
|
||||
has_differences = False
|
||||
|
||||
only_reference_file = True
|
||||
|
||||
file_arr = file_list
|
||||
|
||||
if len(file_list) == 1:
|
||||
file_arr = file_list[0].split()
|
||||
|
||||
base_dir = os.path.abspath(
|
||||
os.path.join(os.getcwd(), "frontend", "public", "locales")
|
||||
)
|
||||
|
||||
for file_path in file_arr:
|
||||
file_normpath = os.path.normpath(file_path)
|
||||
absolute_path = os.path.abspath(file_normpath)
|
||||
|
||||
# Verify that file is within the expected directory
|
||||
if not absolute_path.startswith(base_dir):
|
||||
raise ValueError(f"Unsafe file found: {file_normpath}")
|
||||
|
||||
# Verify file size before processing
|
||||
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
|
||||
raise ValueError(
|
||||
f"The file {file_normpath} is too large and could pose a security risk."
|
||||
)
|
||||
|
||||
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
|
||||
locale_dir = os.path.basename(os.path.dirname(file_normpath))
|
||||
|
||||
if (
|
||||
basename_current_file == basename_reference_file
|
||||
and locale_dir == "en-GB"
|
||||
):
|
||||
continue
|
||||
|
||||
if not file_normpath.endswith(".json") or basename_current_file != "translation.json":
|
||||
continue
|
||||
|
||||
only_reference_file = False
|
||||
report.append(f"#### 📃 **File Check:** `{locale_dir}/{basename_current_file}`")
|
||||
current_keys = read_json_keys(os.path.join(branch, file_path))
|
||||
reference_key_count = len(reference_keys)
|
||||
current_key_count = len(current_keys)
|
||||
|
||||
if reference_key_count != current_key_count:
|
||||
report.append("")
|
||||
report.append("1. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
has_differences = True
|
||||
if reference_key_count > current_key_count:
|
||||
report.append(
|
||||
f" - **_Mismatched key count_**: {reference_key_count} (reference) vs {current_key_count} (current). Translation keys are missing."
|
||||
)
|
||||
elif reference_key_count < current_key_count:
|
||||
report.append(
|
||||
f" - **_Too many keys_**: {reference_key_count} (reference) vs {current_key_count} (current). Please verify if there are additional keys that need to be removed."
|
||||
)
|
||||
else:
|
||||
report.append("1. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
# Check for missing or extra keys
|
||||
current_keys_set = set(current_keys.keys())
|
||||
reference_keys_set = set(reference_keys.keys())
|
||||
missing_keys = current_keys_set.difference(reference_keys_set)
|
||||
extra_keys = reference_keys_set.difference(current_keys_set)
|
||||
missing_keys_list = list(missing_keys)
|
||||
extra_keys_list = list(extra_keys)
|
||||
|
||||
if missing_keys_list or extra_keys_list:
|
||||
has_differences = True
|
||||
missing_keys_str = "`, `".join(missing_keys_list)
|
||||
extra_keys_str = "`, `".join(extra_keys_list)
|
||||
report.append("2. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
if missing_keys_list:
|
||||
report.append(
|
||||
f" - **_Extra keys in `{locale_dir}/{basename_current_file}`_**: `{missing_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
if extra_keys_list:
|
||||
report.append(
|
||||
f" - **_Missing keys in `{locale_dir}/{basename_current_file}`_**: `{extra_keys_str}` that are not present in **_`{basename_reference_file}`_**."
|
||||
)
|
||||
else:
|
||||
report.append("2. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
if find_duplicate_keys(os.path.join(branch, file_normpath)):
|
||||
has_differences = True
|
||||
output = "\n".join(
|
||||
[
|
||||
f" - `{key}`: first at {first}, duplicate at `{duplicate}`"
|
||||
for key, first, duplicate in find_duplicate_keys(
|
||||
os.path.join(branch, file_normpath)
|
||||
)
|
||||
]
|
||||
)
|
||||
report.append("3. **Test Status:** ❌ **_Failed_**")
|
||||
report.append(" - **Issue:**")
|
||||
report.append(" - duplicate entries were found:")
|
||||
report.append(output)
|
||||
else:
|
||||
report.append("3. **Test Status:** ✅ **_Passed_**")
|
||||
|
||||
report.append("")
|
||||
report.append("---")
|
||||
report.append("")
|
||||
|
||||
if has_differences:
|
||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.json](https://github.com/Stirling-Tools/Stirling-PDF/blob/V2/frontend/public/locales/en-GB/translation.json)"
|
||||
)
|
||||
else:
|
||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||
report.append("")
|
||||
report.append(
|
||||
f"Thanks @{actor} for your help in keeping the translations up to date."
|
||||
)
|
||||
|
||||
if not only_reference_file:
|
||||
print("\n".join(report))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Find missing keys")
|
||||
parser.add_argument(
|
||||
"--actor",
|
||||
required=False,
|
||||
help="Actor from PR.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--reference-file",
|
||||
required=True,
|
||||
help="Path to the reference file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--branch",
|
||||
type=str,
|
||||
required=True,
|
||||
help="Branch name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-file",
|
||||
type=str,
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--files",
|
||||
nargs="+",
|
||||
required=False,
|
||||
help="List of changed files, separated by spaces.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Sanitize --actor input to avoid injection attacks
|
||||
if args.actor:
|
||||
args.actor = re.sub(r"[^a-zA-Z0-9_\\-]", "", args.actor)
|
||||
|
||||
# Sanitize --branch input to avoid injection attacks
|
||||
if args.branch:
|
||||
args.branch = re.sub(r"[^a-zA-Z0-9\\-]", "", args.branch)
|
||||
|
||||
file_list = args.files
|
||||
if file_list is None:
|
||||
if args.check_file:
|
||||
file_list = [args.check_file]
|
||||
else:
|
||||
file_list = glob.glob(
|
||||
os.path.join(
|
||||
os.getcwd(),
|
||||
"frontend",
|
||||
"public",
|
||||
"locales",
|
||||
"*",
|
||||
"translation.json",
|
||||
)
|
||||
)
|
||||
update_missing_keys(args.reference_file, file_list)
|
||||
else:
|
||||
check_for_differences(args.reference_file, file_list, args.branch, args.actor)
|
||||
@@ -12,6 +12,7 @@ on:
|
||||
branches:
|
||||
- V2
|
||||
paths:
|
||||
- ".github/workflows/frontend-licenses-update.yml"
|
||||
- "frontend/package.json"
|
||||
- "frontend/package-lock.json"
|
||||
- "frontend/scripts/generate-licenses.js"
|
||||
@@ -28,12 +29,12 @@ jobs:
|
||||
repository-projects: write # Required for enabling automerge
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout PR head (default)
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -48,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Checkout BASE branch (safe script)
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
@@ -56,9 +57,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '22'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
@@ -114,7 +115,7 @@ jobs:
|
||||
# PR Event: Check licenses and comment on PR
|
||||
- name: Delete previous license check comments
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
@@ -167,7 +168,7 @@ jobs:
|
||||
|
||||
- name: Comment on PR - License Check Results
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||
script: |
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
name: Sync Files V2
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- V2
|
||||
- syncLangTest
|
||||
paths:
|
||||
- "build.gradle"
|
||||
- "README.md"
|
||||
- "frontend/public/locales/*/translation.json"
|
||||
- "app/core/src/main/resources/static/3rdPartyLicenses.json"
|
||||
- "scripts/ignore_translation.toml"
|
||||
|
||||
# cancel in-progress jobs if a new job is triggered
|
||||
# This is useful to avoid running multiple builds for the same branch if a new commit is pushed
|
||||
# or a pull request is updated.
|
||||
# It helps to save resources and time by ensuring that only the latest commit is built and tested
|
||||
# This is particularly useful for long-running jobs that may take a while to complete.
|
||||
# The `group` is set to a combination of the workflow name, event name, and branch name.
|
||||
# This ensures that jobs are grouped by the workflow and branch, allowing for cancellation of
|
||||
# in-progress jobs when a new commit is pushed to the same branch or a new pull request is opened.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref_name || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync-files:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@ec9f2d5744a09debf3a187a3f4f675c53b671911 # v2.13.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
|
||||
|
||||
- name: Setup GitHub App Bot
|
||||
id: setup-bot
|
||||
uses: ./.github/actions/setup-bot
|
||||
with:
|
||||
app-id: ${{ secrets.GH_APP_ID }}
|
||||
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: "pip" # caching pip dependencies
|
||||
|
||||
- name: Sync translation JSON files
|
||||
run: |
|
||||
python .github/scripts/check_language_json.py --reference-file "frontend/public/locales/en-GB/translation.json" --branch V2
|
||||
|
||||
- name: Commit translation files
|
||||
run: |
|
||||
git add frontend/public/locales/*/translation.json
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync translation files" || echo "No changes detected"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install --require-hashes -r ./.github/scripts/requirements_sync_readme.txt
|
||||
|
||||
- name: Sync README.md
|
||||
run: |
|
||||
python scripts/counter_translation_v2.py
|
||||
|
||||
- name: Run git add
|
||||
run: |
|
||||
git add README.md scripts/ignore_translation.toml
|
||||
git diff --staged --quiet || git commit -m ":memo: Sync README.md & scripts/ignore_translation.toml" || echo "No changes detected"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: always()
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.setup-bot.outputs.token }}
|
||||
commit-message: Update files
|
||||
committer: ${{ steps.setup-bot.outputs.committer }}
|
||||
author: ${{ steps.setup-bot.outputs.committer }}
|
||||
signoff: true
|
||||
branch: sync_readme_v2
|
||||
base: V2
|
||||
title: ":globe_with_meridians: [V2] Sync Translations + Update README Progress Table"
|
||||
body: |
|
||||
### Description of Changes
|
||||
|
||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation for the **V2 branch**. Below are the details of the changes made:
|
||||
|
||||
#### **1. Synchronization of Translation Files**
|
||||
- Updated translation files (`frontend/public/locales/*/translation.json`) to reflect changes in the reference file `en-GB/translation.json`.
|
||||
- Ensured consistency and synchronization across all supported language files.
|
||||
- Highlighted any missing or incomplete translations.
|
||||
|
||||
#### **2. Update README.md**
|
||||
- Generated the translation progress table in `README.md`.
|
||||
- Added a summary of the current translation status for all supported languages.
|
||||
- Included up-to-date statistics on translation coverage.
|
||||
|
||||
#### **Why these changes are necessary**
|
||||
- Keeps translation files aligned with the latest reference updates.
|
||||
- Ensures the documentation reflects the current translation progress.
|
||||
|
||||
---
|
||||
|
||||
Auto-generated by [create-pull-request][1].
|
||||
|
||||
[1]: https://github.com/peter-evans/create-pull-request
|
||||
draft: false
|
||||
delete-branch: true
|
||||
labels: github-actions
|
||||
sign-commits: true
|
||||
add-paths: |
|
||||
README.md
|
||||
frontend/public/locales/*/translation.json
|
||||
@@ -97,7 +97,6 @@ All documentation available at [https://docs.stirlingpdf.com/](https://docs.stir
|
||||
|
||||
|
||||
|
||||
|
||||
# 📖 Get Started
|
||||
|
||||
Visit our comprehensive documentation at [docs.stirlingpdf.com](https://docs.stirlingpdf.com) for:
|
||||
@@ -116,46 +115,46 @@ Stirling-PDF currently supports 40 languages!
|
||||
|
||||
| Language | Progress |
|
||||
| -------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Azerbaijani (Azərbaycan Dili) (az_AZ) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesian (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Persian (فارسی) (fa_IR) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Slovenian (Slovenščina) (sl_SI) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Tibetan (བོད་ཡིག་) (bo_CN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Malayalam (മലയാളം) (ml_IN) |  |
|
||||
|
||||
## Stirling PDF Enterprise
|
||||
|
||||
@@ -4,7 +4,7 @@ FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY frontend/package*.json ./
|
||||
COPY frontend/package.json frontend/package-lock.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci
|
||||
|
||||
Generated
+167
-117
@@ -10,22 +10,24 @@
|
||||
"license": "SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@embedpdf/core": "^1.3.0",
|
||||
"@embedpdf/engines": "^1.2.1",
|
||||
"@embedpdf/plugin-export": "^1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||
"@embedpdf/plugin-loader": "^1.3.0",
|
||||
"@embedpdf/plugin-pan": "^1.3.0",
|
||||
"@embedpdf/plugin-render": "^1.3.0",
|
||||
"@embedpdf/plugin-rotate": "^1.3.0",
|
||||
"@embedpdf/plugin-scroll": "^1.3.0",
|
||||
"@embedpdf/plugin-search": "^1.3.0",
|
||||
"@embedpdf/plugin-selection": "^1.3.0",
|
||||
"@embedpdf/plugin-spread": "^1.3.0",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.0",
|
||||
"@embedpdf/plugin-tiling": "^1.3.0",
|
||||
"@embedpdf/plugin-viewport": "^1.3.0",
|
||||
"@embedpdf/plugin-zoom": "^1.3.0",
|
||||
"@embedpdf/core": "^1.3.1",
|
||||
"@embedpdf/engines": "^1.3.1",
|
||||
"@embedpdf/plugin-annotation": "^1.3.1",
|
||||
"@embedpdf/plugin-export": "^1.3.1",
|
||||
"@embedpdf/plugin-history": "^1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.1",
|
||||
"@embedpdf/plugin-loader": "^1.3.1",
|
||||
"@embedpdf/plugin-pan": "^1.3.1",
|
||||
"@embedpdf/plugin-render": "^1.3.1",
|
||||
"@embedpdf/plugin-rotate": "^1.3.1",
|
||||
"@embedpdf/plugin-scroll": "^1.3.1",
|
||||
"@embedpdf/plugin-search": "^1.3.1",
|
||||
"@embedpdf/plugin-selection": "^1.3.1",
|
||||
"@embedpdf/plugin-spread": "^1.3.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.1",
|
||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
@@ -489,13 +491,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-KEic1NA9JrtNRoTq3O3m93YTglRKweR6uqjzX3sLGCmy+LsUjiH5WOCJAztlSlmZEXysAlZlyzG/09gz4tpBAg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz",
|
||||
"integrity": "sha512-2Az6trhiMMBIv+GFvV8H8UOS1gwQn7NK0KaJMcdsZbUHYLO0P95aVd6Pi/GRzEH4XyF51TDIoTOAUtf07TQ5dQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/engines": "1.3.0",
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/engines": "1.3.1",
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -505,13 +507,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/engines": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.0.tgz",
|
||||
"integrity": "sha512-6WbYwxtCCjOazEMGKbhKRkos6S1VkzI4R2u6dUuIsUw9G2HLP4bwJCBKj9A0FuMAJkKQ3VL5eVCSGfqaCaRoyQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.1.tgz",
|
||||
"integrity": "sha512-G3pI+18la7spviUMuA5s9/hV95jlfkA2+CNxqlHBO5ocw3641E3d36Lv+mx+6yU7k0B5vEOQPZDGRMg7KFziBQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0",
|
||||
"@embedpdf/pdfium": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"@embedpdf/pdfium": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
@@ -521,27 +523,63 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/models": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.0.tgz",
|
||||
"integrity": "sha512-LIY6T+nQoc1hi6nq1NlH6sR43J3PYOg9Bux8ouEnKjEGiZMgyd1cMxhBfrrY+Ft6DsSkqqujFOVEwjeYQYy3dg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.1.tgz",
|
||||
"integrity": "sha512-OzmO1rQAuOP/Y3aYXmW21dPNAx49olhr9ZO2hDdI0fbNBHTVGxnaKqOISxVmUz7TmhTwVBljERACnaA8Ib4b4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/pdfium": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.0.tgz",
|
||||
"integrity": "sha512-rSBFYjxwQ58L/HcqR0l5Vv4G5t+CCOKlFYrDReTZYNN7fhzKPUWbXUn4ARahZWCNmF8svHumV2P4ArakJJviuw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.1.tgz",
|
||||
"integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.0.tgz",
|
||||
"integrity": "sha512-R6VItLmXmXbb0/4AsH1YGUZd0c64K/8kxQd0XAvgUJwcL7Z4s8bLsqRx4sVQqwVllaPEJbAdFn1CC/ymkGB+fg==",
|
||||
"node_modules/@embedpdf/plugin-annotation": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-annotation/-/plugin-annotation-1.3.1.tgz",
|
||||
"integrity": "sha512-mmePRYYBB8v8NIZ95XVfFkpyQ2QiKIGdWyvrPeJXSbL3/K6d6ix+o/jHBVvBWyTsQzdIlzs+FW8+iT0M1zkEow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"@embedpdf/utils": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-history": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-selection": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-export": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-export/-/plugin-export-1.3.1.tgz",
|
||||
"integrity": "sha512-reb03vNPFP5GuIAFExMcuYBVYu/deVO2v8EoCwRZ/lzzYMORIkJjpNWDQPo9VfyGBh1x4/o3CHvxisU1Y1tDLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-history": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-history/-/plugin-history-1.3.1.tgz",
|
||||
"integrity": "sha512-HrPkWQmAk08mbHiOcIN4htVq5KJMqI9zSjAqaYQEhV/TugeHfWVpK+xMst/PzuFb14HWgk5gWXjtV5E4SDlw9w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -549,15 +587,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-interaction-manager": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.0.tgz",
|
||||
"integrity": "sha512-iMG7mW+4YpNjBeSAcC5kK9VnjwmNu71HTxVtKnN73t3EBfukbMH4y7Tp2ds+4I97H6vc18RK5xuUCSesEOBgww==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.3.1.tgz",
|
||||
"integrity": "sha512-8h3y5a9tQ1fZlc4mP1/+XKyuHWwcQEm9AujKxy+6f6omtCBzpnKrH95bURgYOzQEBGY7d5C3HvG6JOlh0o1x3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -565,15 +603,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-loader": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.0.tgz",
|
||||
"integrity": "sha512-tkOa1UwFOimueSxxm2hRAAh64K75itDvUO6wHjb5X5s0Hx4DccfrJ7KusDhxBkeQLFXtZknPG0Q2/9T+joAqeQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.3.1.tgz",
|
||||
"integrity": "sha512-NjNmA7TOs3E/zwb9I+YohzyGkxq8y5NUGu0MKgh2g41lZoFvyqTAjFPar+RjEiLX8iiJiwNZswyJsNrytmS3Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -581,17 +619,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-pan": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.0.tgz",
|
||||
"integrity": "sha512-tZxUpX9dvd/VDHCTqM9Yjss4M8pkJWFUA5GDNmPkExRXIASuB98wEP8fh0rQt13TEZ30rV77cEsNXngju56kjg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.3.1.tgz",
|
||||
"integrity": "sha512-lF1gkz/a77G3+Rr8MOefkGnPJ1i5xWnClXm2ZzYAl7PbOScp59/PaP7qeU7eMPC4FHQM81ZhCgVYGXogbaB8ww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -599,15 +637,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-render": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.0.tgz",
|
||||
"integrity": "sha512-ZyxoGIIUa2HBLt1IB64EdWqBxHh01AX/1HJ7/cnoQK1h/oKXRbMAX6Mb23JCh2PGa4sGeyV3psoFMt037Eew3g==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.3.1.tgz",
|
||||
"integrity": "sha512-c9oH097e1CVUpYF9RgZRfV/7XCJ0pf+svdT1wyM2MbWby06ti20oCwT9wf7BLY0hPQ7+eO3wunr1I1/y3MnVrw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -615,15 +653,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-rotate": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.0.tgz",
|
||||
"integrity": "sha512-EyLLwf9VKQCsMRTe0KwGe+ZAaFqmcYS5WW/qqPBNfvSuBaybNpdI+C72IQFr41X7cYQV58OgEL3bfDb1MBPGHQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.3.1.tgz",
|
||||
"integrity": "sha512-mRAlIW7IZAnCyDuYqN13yDc6yoNIYLUB4uYTUAR7vTIt021C8H5jDHk9TmLwcH0tQ8/R3yHuDm/XPAe0zfs81g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -631,16 +669,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-scroll": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.0.tgz",
|
||||
"integrity": "sha512-o1n6Mkoc92BHAkoCX0mSLXgOj4uAkokNbvP+2QMijShzTsl95gU5UzK6siZ5o6WgZBznJcceYmLuPR5ODqZDWg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.1.tgz",
|
||||
"integrity": "sha512-mDvK3DyBZC8/8pOEdJsWtSjCmV2ZuZJJ6xfspJpsaDVywo1Vq6M55BtKThkhqED6mqbFWTN9rP9cbWG8KDBWVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -648,16 +686,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-search": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.0.tgz",
|
||||
"integrity": "sha512-DilSRfPQR38picjx7eyyuXNeduD7hcW/PjT9DZrjXxfLrAQtd17CXJs7HtJevl1wErh/CCSvZlHhjp1++O6GAg==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.1.tgz",
|
||||
"integrity": "sha512-SLwYPQg1NJWytq2sd4MnWFmRVGgzwbohBedB2kH0ALsvdnoRYqgjR5HqAsKgoRJO/pphQhHlk3L1gLW62r6hqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-loader": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-loader": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -665,17 +703,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-selection": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.0.tgz",
|
||||
"integrity": "sha512-1PEtreNofysaLxZvgO2CSNCxXhevjYnBdu4IHTFeJKXoq3ckKwkX8fJjyyN4D6+6uXZsnFkHhewl1yKCfKWAWw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.1.tgz",
|
||||
"integrity": "sha512-yef2XB/zR7zjyeUB3Ul0SbTcXqu5isR0GtINkFwL7bJMok6HpYNDnMXSuo55BaxI0dOCnnCSZfoRkAgosnZ1uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -683,16 +721,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-spread": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.0.tgz",
|
||||
"integrity": "sha512-oRLimcod8RhdknN94CQeG+0QndQeiZKIhFUCXDIGxN1Z/qvspZCUty2TC+1kc3G318nZi55pWWphq9sB7ZpqEw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.3.1.tgz",
|
||||
"integrity": "sha512-RJ/kgJsFRdtWlPMXTW1feUSb6WHIvxtNRLgqzX8dlFIoyc4oZex2Vw+URo/VZuWSe/NvCIihQ20rkNAQJMnNMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-loader": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-loader": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -700,34 +738,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-thumbnail": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.0.tgz",
|
||||
"integrity": "sha512-w2wzL7m6/sUF54sMVEi8Y8+7VE3BcZqI8THDqobkEkno4Dgmb77FHNPFD6YtAhaRmIoyPnlZf05RDd6Z8ohhkQ==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.3.1.tgz",
|
||||
"integrity": "sha512-xv96ESa7JgD5z+TzcOK18/u0gq3d9v7QPv2wpr0ZhcnwLwf4sH0eUJZIsv7z7DMOpBNz7o7jJbrtxDUdCEHGhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-render": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-render": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-tiling": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.0.tgz",
|
||||
"integrity": "sha512-huYi4BJa9KSfqC424bEHw72KBLCR2rfApMeKnpUzAFSdWA6MSYmVBSk8ghnU7XbcLuL6fFBarNsziNrSSnVWTw==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.1.tgz",
|
||||
"integrity": "sha512-Q8RF80fb6y9GDAKwvgsu0BsWJlQuhNCtSKWwp3YcZJtIBFm94DVcg0zTgvDmE9/WNOmn4Z1Edt86usmYauHolw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-render": "1.3.0",
|
||||
"@embedpdf/plugin-scroll": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-render": "1.3.1",
|
||||
"@embedpdf/plugin-scroll": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -735,15 +773,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-viewport": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.0.tgz",
|
||||
"integrity": "sha512-AZ7U8DEgEQ8nK5kdrqtukLl5au9NE3mIlFmloyo6Ddrt2rN/Jw1Lt9dsl6wU20GcFQX+hWsg9uAJboLq6AdOCA==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.3.1.tgz",
|
||||
"integrity": "sha512-gzosrWL18ZhN175Kxocf/p7uqYBhNHvEuV1CpJQmN7ys48aew6Qq8z7MjAsCnJBANXk/8syNdo3qWwBriyjQNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0"
|
||||
"@embedpdf/models": "1.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
@@ -751,19 +789,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/plugin-zoom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.0.tgz",
|
||||
"integrity": "sha512-1VA9aFxoP+BoEpwlR0//jtlD9ESS8nhU8OGGHBRu7IgoWzIx4GqOHgpgXVxzFl9IaLOv69E9DVmwe/yaC6F+0g==",
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.1.tgz",
|
||||
"integrity": "sha512-3GXpgv6XmZiQnjaPbsxblTqUn84ALFiyONh2gwrEU9apB6STT3TQiY0QRindwrUXdQLpCSjRSB9PpDBCtTww7w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@embedpdf/models": "1.3.0",
|
||||
"@embedpdf/models": "1.3.1",
|
||||
"hammerjs": "^2.0.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@embedpdf/core": "1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.0",
|
||||
"@embedpdf/plugin-scroll": "1.3.0",
|
||||
"@embedpdf/plugin-viewport": "1.3.0",
|
||||
"@embedpdf/core": "1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "1.3.1",
|
||||
"@embedpdf/plugin-scroll": "1.3.1",
|
||||
"@embedpdf/plugin-viewport": "1.3.1",
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"vue": ">=3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@embedpdf/utils": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@embedpdf/utils/-/utils-1.3.1.tgz",
|
||||
"integrity": "sha512-6trYysnggwCCTB2q7cX6tkOTbZJNtt2YYZohPCmh0yaDpkfNSgwDwD0jCLtEU2UZLQoH4+2GvNo+4xe+KAGlIQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"preact": "^10.26.4",
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
|
||||
+18
-16
@@ -6,22 +6,24 @@
|
||||
"proxy": "http://localhost:8080",
|
||||
"dependencies": {
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
|
||||
"@embedpdf/core": "^1.3.0",
|
||||
"@embedpdf/engines": "^1.2.1",
|
||||
"@embedpdf/plugin-export": "^1.3.0",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.0",
|
||||
"@embedpdf/plugin-loader": "^1.3.0",
|
||||
"@embedpdf/plugin-pan": "^1.3.0",
|
||||
"@embedpdf/plugin-render": "^1.3.0",
|
||||
"@embedpdf/plugin-rotate": "^1.3.0",
|
||||
"@embedpdf/plugin-scroll": "^1.3.0",
|
||||
"@embedpdf/plugin-search": "^1.3.0",
|
||||
"@embedpdf/plugin-selection": "^1.3.0",
|
||||
"@embedpdf/plugin-spread": "^1.3.0",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.0",
|
||||
"@embedpdf/plugin-tiling": "^1.3.0",
|
||||
"@embedpdf/plugin-viewport": "^1.3.0",
|
||||
"@embedpdf/plugin-zoom": "^1.3.0",
|
||||
"@embedpdf/core": "^1.3.1",
|
||||
"@embedpdf/engines": "^1.3.1",
|
||||
"@embedpdf/plugin-annotation": "^1.3.1",
|
||||
"@embedpdf/plugin-export": "^1.3.1",
|
||||
"@embedpdf/plugin-history": "^1.3.1",
|
||||
"@embedpdf/plugin-interaction-manager": "^1.3.1",
|
||||
"@embedpdf/plugin-loader": "^1.3.1",
|
||||
"@embedpdf/plugin-pan": "^1.3.1",
|
||||
"@embedpdf/plugin-render": "^1.3.1",
|
||||
"@embedpdf/plugin-rotate": "^1.3.1",
|
||||
"@embedpdf/plugin-scroll": "^1.3.1",
|
||||
"@embedpdf/plugin-search": "^1.3.1",
|
||||
"@embedpdf/plugin-selection": "^1.3.1",
|
||||
"@embedpdf/plugin-spread": "^1.3.1",
|
||||
"@embedpdf/plugin-thumbnail": "^1.3.1",
|
||||
"@embedpdf/plugin-tiling": "^1.3.1",
|
||||
"@embedpdf/plugin-viewport": "^1.3.1",
|
||||
"@embedpdf/plugin-zoom": "^1.3.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@iconify/react": "^6.0.2",
|
||||
|
||||
@@ -87,7 +87,10 @@
|
||||
"showStack": "Stack-Trace anzeigen",
|
||||
"copyStack": "Stack-Trace kopieren",
|
||||
"githubSubmit": "GitHub - Ein Ticket einreichen",
|
||||
"discordSubmit": "Discord - Unterstützungsbeitrag einreichen"
|
||||
"discordSubmit": "Discord - Unterstützungsbeitrag einreichen",
|
||||
"dismissAllErrors": "Alle Fehler ausblenden",
|
||||
"encryptedPdfMustRemovePassword": "Diese PDF ist verschlüsselt oder passwortgeschützt. Bitte entsperren Sie sie, bevor Sie in PDF/A konvertieren.",
|
||||
"incorrectPasswordProvided": "Das PDF-Passwort ist falsch oder wurde nicht angegeben."
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "Warnung"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "Sortieren nach:",
|
||||
"multiTool": {
|
||||
"title": "PDF-Multitool",
|
||||
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen"
|
||||
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen",
|
||||
"tags": "mehrere,werkzeuge"
|
||||
},
|
||||
"merge": {
|
||||
"title": "Zusammenführen",
|
||||
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen"
|
||||
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen",
|
||||
"tags": "kombinieren,zusammenführen,vereinen"
|
||||
},
|
||||
"split": {
|
||||
"title": "Aufteilen",
|
||||
"desc": "PDFs in mehrere Dokumente aufteilen"
|
||||
"desc": "PDFs in mehrere Dokumente aufteilen",
|
||||
"tags": "teilen,trennen,aufteilen"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "Drehen",
|
||||
"desc": "Drehen Sie Ihre PDFs ganz einfach"
|
||||
"desc": "Drehen Sie Ihre PDFs ganz einfach",
|
||||
"tags": "drehen,spiegeln,ausrichten"
|
||||
},
|
||||
"convert": {
|
||||
"title": "Umwandeln",
|
||||
"desc": "Dateien zwischen verschiedenen Formaten konvertieren"
|
||||
"desc": "Dateien zwischen verschiedenen Formaten konvertieren",
|
||||
"tags": "umwandeln,ändern"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "Organisieren",
|
||||
"desc": "Seiten entfernen und Seitenreihenfolge ändern"
|
||||
"desc": "Seiten entfernen und Seitenreihenfolge ändern",
|
||||
"tags": "organisieren,umordnen,neu anordnen"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "Bild einfügen",
|
||||
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)"
|
||||
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)",
|
||||
"tags": "einfügen,einbetten,platzieren"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "Anhänge hinzufügen",
|
||||
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen"
|
||||
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen",
|
||||
"tags": "einbetten,anhängen,einfügen"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Wasserzeichen hinzufügen",
|
||||
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu"
|
||||
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu",
|
||||
"tags": "stempel,markierung,überlagerung"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "Passwort entfernen",
|
||||
"desc": "Den Passwortschutz eines PDFs entfernen"
|
||||
"desc": "Den Passwortschutz eines PDFs entfernen",
|
||||
"tags": "entsperren"
|
||||
},
|
||||
"compress": {
|
||||
"title": "Komprimieren",
|
||||
"desc": "PDF komprimieren um die Dateigröße zu reduzieren"
|
||||
"desc": "PDF komprimieren um die Dateigröße zu reduzieren",
|
||||
"tags": "verkleinern,reduzieren,optimieren"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Schreibgeschützte PDF-Formfelder entfernen",
|
||||
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument."
|
||||
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument.",
|
||||
"tags": "entsperren,aktivieren,bearbeiten"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "Metadaten ändern",
|
||||
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument"
|
||||
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument",
|
||||
"tags": "bearbeiten,ändern,aktualisieren"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "Führe OCR/Cleanup-Scans aus",
|
||||
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu"
|
||||
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu",
|
||||
"tags": "extrahieren,scannen"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "Bilder extrahieren",
|
||||
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv"
|
||||
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv",
|
||||
"tags": "extrahieren,speichern,exportieren"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "Gescannte Fotos erkennen/aufteilen",
|
||||
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf"
|
||||
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf",
|
||||
"tags": "erkennen,teilen,fotos"
|
||||
},
|
||||
"sign": {
|
||||
"title": "Signieren",
|
||||
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu"
|
||||
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu",
|
||||
"tags": "unterschrift,autogramm"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Abflachen",
|
||||
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen"
|
||||
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen",
|
||||
"tags": "vereinfachen,entfernen,interaktiv"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "Mit Zertifikat signieren",
|
||||
"desc": "Ein PDF mit einem Zertifikat/Schlüssel (PEM/P12) signieren"
|
||||
"desc": "Ein PDF mit einem Zertifikat/Schlüssel (PEM/P12) signieren",
|
||||
"tags": "authentifizieren,PEM,P12,offiziell,verschlüsseln,signieren,zertifikat,PKCS12,JKS,server,manuell,auto"
|
||||
},
|
||||
"repair": {
|
||||
"title": "Reparatur",
|
||||
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren"
|
||||
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren",
|
||||
"tags": "reparieren,wiederherstellen"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "Leere Seiten entfernen",
|
||||
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument"
|
||||
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument",
|
||||
"tags": "löschen,bereinigen,leer"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "Anmerkungen entfernen",
|
||||
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF"
|
||||
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF",
|
||||
"tags": "löschen,bereinigen,entfernen"
|
||||
},
|
||||
"compare": {
|
||||
"title": "Vergleichen",
|
||||
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an"
|
||||
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an",
|
||||
"tags": "unterschied"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "Zertifikatsignatur entfernen",
|
||||
"desc": "Zertifikatsignatur aus PDF entfernen"
|
||||
"desc": "Zertifikatsignatur aus PDF entfernen",
|
||||
"tags": "entfernen,löschen,entsperren"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "Mehrseitiges Layout",
|
||||
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen"
|
||||
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen",
|
||||
"tags": "layout,anordnen,kombinieren"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Broschüren-Layout",
|
||||
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen"
|
||||
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen",
|
||||
"tags": "broschüre,druck,bindung"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Seitengröße/Skalierung anpassen",
|
||||
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern"
|
||||
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern",
|
||||
"tags": "größe ändern,anpassen,skalieren"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "Seitenzahlen hinzufügen",
|
||||
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle"
|
||||
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle",
|
||||
"tags": "nummerieren,paginierung,zählen"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "PDF-Datei automatisch umbenennen",
|
||||
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um"
|
||||
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um",
|
||||
"tags": "auto-erkennung,kopfzeilen-basiert,organisieren,umbenennen"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Farben/Kontrast anpassen",
|
||||
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen"
|
||||
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen",
|
||||
"tags": "kontrast,helligkeit,sättigung"
|
||||
},
|
||||
"crop": {
|
||||
"title": "PDF zuschneiden",
|
||||
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)"
|
||||
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)",
|
||||
"tags": "zuschneiden,schneiden,größe ändern"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "PDF automatisch teilen",
|
||||
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen"
|
||||
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen",
|
||||
"tags": "auto,teilen,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Bereinigen",
|
||||
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen"
|
||||
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen",
|
||||
"tags": "bereinigen,löschen,entfernen"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "Alle Informationen anzeigen",
|
||||
"desc": "Erfasst alle möglichen Informationen in einer PDF"
|
||||
"desc": "Erfasst alle möglichen Informationen in einer PDF",
|
||||
"tags": "info,metadaten,details"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF zu einer Seite zusammenfassen",
|
||||
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen"
|
||||
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen",
|
||||
"tags": "kombinieren,zusammenführen,einzeln"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "Javascript anzeigen",
|
||||
"desc": "Alle Javascript Funktionen in einer PDF anzeigen"
|
||||
"desc": "Alle Javascript Funktionen in einer PDF anzeigen",
|
||||
"tags": "javascript,code,skript"
|
||||
},
|
||||
"redact": {
|
||||
"title": "Manuell zensieren/schwärzen",
|
||||
"desc": "Zensiere (Schwärze) eine PDF-Datei durch Auswählen von Text, gezeichneten Formen und/oder ausgewählten Seite(n)"
|
||||
"desc": "Zensiere (Schwärze) eine PDF-Datei durch Auswählen von Text, gezeichneten Formen und/oder ausgewählten Seite(n)",
|
||||
"tags": "zensieren,schwärzen,verbergen"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "PDFs überlagern",
|
||||
"desc": "PDFs über eine andere PDF überlagern"
|
||||
"desc": "PDFs über eine andere PDF überlagern",
|
||||
"tags": "überlagern,kombinieren,stapeln"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "PDF nach Abschnitten aufteilen",
|
||||
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen"
|
||||
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen",
|
||||
"tags": "teilen,abschnitte,aufteilen"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "Stempel zu PDF hinzufügen",
|
||||
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen"
|
||||
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen",
|
||||
"tags": "stempel,markierung,siegel"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "Bild entfernen",
|
||||
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern"
|
||||
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern",
|
||||
"tags": "entfernen,löschen,bereinigen"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "PDF-Datei nach Kapiteln aufteilen",
|
||||
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur."
|
||||
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.",
|
||||
"tags": "teilen,kapitel,struktur"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "PDF-Signatur überprüfen",
|
||||
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen"
|
||||
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen",
|
||||
"tags": "validieren,überprüfen,zertifikat"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API-Dokumentation",
|
||||
"desc": "API-Dokumentation anzeigen und Endpunkte testen"
|
||||
"desc": "API-Dokumentation anzeigen und Endpunkte testen",
|
||||
"tags": "API,dokumentation,test"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Scan simulieren",
|
||||
@@ -538,42 +585,52 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Inhaltsverzeichnis bearbeiten",
|
||||
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten"
|
||||
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten",
|
||||
"tags": "lesezeichen,inhalt,bearbeiten"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Zertifikate verwalten",
|
||||
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen."
|
||||
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen.",
|
||||
"tags": "zertifikate,importieren,exportieren"
|
||||
},
|
||||
"read": {
|
||||
"title": "Lesen",
|
||||
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen."
|
||||
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen.",
|
||||
"tags": "anzeigen,öffnen,anzeigen"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Seiten neu anordnen",
|
||||
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen."
|
||||
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen.",
|
||||
"tags": "umordnen,neu anordnen,organisieren"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "Seiten extrahieren",
|
||||
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren"
|
||||
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren",
|
||||
"tags": "extrahieren,auswählen,kopieren"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "Entfernen",
|
||||
"desc": "Ungewollte Seiten aus dem PDF entfernen"
|
||||
"desc": "Ungewollte Seiten aus dem PDF entfernen",
|
||||
"tags": "löschen,extrahieren,ausschließen"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "Teilen nach Größe/Anzahl",
|
||||
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf"
|
||||
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf",
|
||||
"tags": "auto,teilen,größe"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Farbe ersetzen und invertieren",
|
||||
"desc": "Ersetzen Sie die Farbe des Texts und Hintergrund der PDF-Datei und invertieren Sie die komplette Farbe der PDF-Datei, um die Dateigröße zu reduzieren"
|
||||
},
|
||||
"devApi": {
|
||||
"desc": "Link zur API-Dokumentation"
|
||||
"desc": "Link zur API-Dokumentation",
|
||||
"tags": "API,entwicklung,dokumentation",
|
||||
"title": "API"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "Automatische Ordnerüberwachung",
|
||||
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen"
|
||||
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen",
|
||||
"tags": "automatisierung,ordner,scannen"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "SSO-Anleitung",
|
||||
@@ -593,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automatisieren",
|
||||
"desc": "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben."
|
||||
"desc": "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben.",
|
||||
"tags": "arbeitsablauf,sequenz,automatisierung"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "Farben in PDF-Dokumenten ersetzen oder invertieren",
|
||||
"title": "Farbe ersetzen & invertieren"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "Erstellen Sie eine PDF, die aussieht, als wäre sie gescannt worden",
|
||||
"tags": "scannen,simulieren,erstellen",
|
||||
"title": "Scanner-Effekt"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -633,8 +700,18 @@
|
||||
"merge": {
|
||||
"tags": "zusammenführen,seitenvorgänge,back end,serverseitig",
|
||||
"title": "Zusammenführen",
|
||||
"removeDigitalSignature": "Digitale Signatur in der zusammengeführten Datei entfernen?",
|
||||
"generateTableOfContents": "Inhaltsverzeichnis in der zusammengeführten Datei erstellen?",
|
||||
"removeDigitalSignature": {
|
||||
"tooltip": {
|
||||
"description": "Digitale Signaturen werden beim Zusammenführen von Dateien ungültig. Aktivieren Sie diese Option, um sie aus der endgültigen zusammengeführten PDF zu entfernen.",
|
||||
"title": "Digitale Signatur entfernen"
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": {
|
||||
"tooltip": {
|
||||
"description": "Erstellt automatisch ein klickbares Inhaltsverzeichnis in der zusammengeführten PDF basierend auf den ursprünglichen Dateinamen und Seitenzahlen.",
|
||||
"title": "Inhaltsverzeichnis generieren"
|
||||
}
|
||||
},
|
||||
"submit": "Zusammenführen",
|
||||
"sortBy": {
|
||||
"description": "Dateien werden in der Reihenfolge zusammengeführt, in der sie ausgewählt wurden. Ziehen Sie zum Neuordnen oder sortieren Sie unten.",
|
||||
@@ -860,7 +937,13 @@
|
||||
"images": "Bilder",
|
||||
"officeDocs": "Office-Dokumente (Word, Excel, PowerPoint)",
|
||||
"imagesExt": "Bilder (JPG, PNG, usw.)",
|
||||
"grayscale": "Graustufen"
|
||||
"grayscale": "Graustufen",
|
||||
"dpi": "DPI",
|
||||
"markdown": "Markdown",
|
||||
"odtExt": "OpenDocument Text (.odt)",
|
||||
"pptExt": "PowerPoint (.pptx)",
|
||||
"rtfExt": "Rich Text Format (.rtf)",
|
||||
"textRtf": "Text/RTF"
|
||||
},
|
||||
"imageToPdf": {
|
||||
"tags": "konvertierung,img,jpg,bild,foto"
|
||||
@@ -900,7 +983,20 @@
|
||||
"10": "Ungerade-Gerade-Zusammenführung",
|
||||
"11": "Alle Seiten duplizieren"
|
||||
},
|
||||
"placeholder": "(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)"
|
||||
"placeholder": "(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "Seiten für den Broschüren-Druck anordnen (letzte, erste, zweite, vorletzte, …).",
|
||||
"CUSTOM": "Verwenden Sie eine benutzerdefinierte Sequenz von Seitenzahlen oder Ausdrücken, um eine neue Reihenfolge zu definieren.",
|
||||
"DUPLEX_SORT": "Vorder- und Rückseiten verschachteln, als ob ein Duplex-Scanner alle Vorderseiten und dann alle Rückseiten gescannt hätte (1, n, 2, n-1, …).",
|
||||
"DUPLICATE": "Jede Seite entsprechend der benutzerdefinierten Anzahl duplizieren (z.B. 4 dupliziert jede Seite 4×).",
|
||||
"ODD_EVEN_MERGE": "Zwei PDFs durch abwechselnde Seiten zusammenführen: ungerade aus der ersten, gerade aus der zweiten.",
|
||||
"ODD_EVEN_SPLIT": "Das Dokument in zwei Ausgaben aufteilen: alle ungeraden Seiten und alle geraden Seiten.",
|
||||
"REMOVE_FIRST": "Die erste Seite aus dem Dokument entfernen.",
|
||||
"REMOVE_FIRST_AND_LAST": "Sowohl die erste als auch die letzte Seite aus dem Dokument entfernen.",
|
||||
"REMOVE_LAST": "Die letzte Seite aus dem Dokument entfernen.",
|
||||
"REVERSE_ORDER": "Das Dokument umkehren, sodass die letzte Seite zur ersten wird usw.",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "Seiten für den Seitenheft-Broschüren-Druck anordnen (optimiert für die Bindung an der Seite)."
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "img,jpg,bild,foto",
|
||||
@@ -929,7 +1025,8 @@
|
||||
"failed": "Ein Fehler ist beim Hinzufügen des Wasserzeichens zur PDF aufgetreten."
|
||||
},
|
||||
"watermarkType": {
|
||||
"image": "Bild"
|
||||
"image": "Bild",
|
||||
"text": "Text"
|
||||
},
|
||||
"settings": {
|
||||
"type": "Wasserzeichen-Typ",
|
||||
@@ -1333,7 +1430,9 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "Trapped-Status",
|
||||
"unknown": "Unbekannt"
|
||||
"unknown": "Unbekannt",
|
||||
"false": "Falsch",
|
||||
"true": "Wahr"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweiterte Optionen"
|
||||
@@ -1522,7 +1621,13 @@
|
||||
"header": "Bilder extrahieren",
|
||||
"selectText": "Wählen Sie das Bildformat aus, in das extrahierte Bilder konvertiert werden sollen",
|
||||
"allowDuplicates": "Doppelte Bilder speichern",
|
||||
"submit": "Extrahieren"
|
||||
"submit": "Extrahieren",
|
||||
"error": {
|
||||
"failed": "Beim Extrahieren der Bilder aus der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "archiv,langfristig,standard,konvertierung,speicherung,aufbewahrung",
|
||||
@@ -1599,8 +1704,14 @@
|
||||
"title": "Signieren",
|
||||
"header": "PDFs signieren",
|
||||
"upload": "Bild hochladen",
|
||||
"draw": "Signatur zeichnen",
|
||||
"text": "Texteingabe",
|
||||
"draw": {
|
||||
"clear": "Löschen",
|
||||
"title": "Zeichnen Sie Ihre Unterschrift"
|
||||
},
|
||||
"text": {
|
||||
"name": "Name des Unterzeichners",
|
||||
"placeholder": "Geben Sie Ihren vollständigen Namen ein"
|
||||
},
|
||||
"clear": "Leeren",
|
||||
"add": "Signieren",
|
||||
"saved": "Gespeicherte Signaturen",
|
||||
@@ -1616,7 +1727,35 @@
|
||||
"previous": "Vorherige Seite",
|
||||
"maintainRatio": "Seitenverhältnis beibehalten ein-/ausschalten",
|
||||
"undo": "Rückgängig",
|
||||
"redo": "Wiederherstellen"
|
||||
"redo": "Wiederherstellen",
|
||||
"activate": "Signatur-Platzierung aktivieren",
|
||||
"applySignatures": "Signaturen anwenden",
|
||||
"deactivate": "Signatur-Platzierung beenden",
|
||||
"error": {
|
||||
"failed": "Beim Signieren der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"image": {
|
||||
"hint": "Laden Sie ein PNG- oder JPG-Bild Ihrer Unterschrift hoch",
|
||||
"label": "Unterschriftsbild hochladen",
|
||||
"placeholder": "Bilddatei auswählen"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "So fügen Sie eine Unterschrift hinzu"
|
||||
},
|
||||
"results": {
|
||||
"title": "Signatur-Ergebnisse"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "Signatur konfigurieren"
|
||||
},
|
||||
"submit": "Dokument signieren",
|
||||
"type": {
|
||||
"canvas": "Canvas",
|
||||
"draw": "Zeichnen",
|
||||
"image": "Bild",
|
||||
"text": "Text",
|
||||
"title": "Signaturtyp"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "statisch,deaktivieren,nicht interaktiv,optimieren",
|
||||
@@ -1635,7 +1774,8 @@
|
||||
"stepTitle": "Abflachungs-Optionen",
|
||||
"title": "Abflachungs-Optionen",
|
||||
"flattenOnlyForms.desc": "Nur Formularfelder vereinfachen, andere interaktive Elemente unverändert lassen",
|
||||
"note": "Das Abflachen entfernt interaktive Elemente aus der PDF und macht sie nicht mehr bearbeitbar."
|
||||
"note": "Das Abflachen entfernt interaktive Elemente aus der PDF und macht sie nicht mehr bearbeitbar.",
|
||||
"flattenOnlyForms": "Nur Formulare vereinfachen"
|
||||
},
|
||||
"results": {
|
||||
"title": "Reduzierungs-Ergebnisse"
|
||||
@@ -1693,7 +1833,8 @@
|
||||
"label": "Pixel-Weißheitsschwellwert"
|
||||
},
|
||||
"whitePercent": {
|
||||
"label": "Weiß-Prozentsatz-Schwellwert"
|
||||
"label": "Weiß-Prozentsatz-Schwellwert",
|
||||
"unit": "%"
|
||||
},
|
||||
"includeBlankPages": {
|
||||
"label": "Erkannte leere Seiten einschließen"
|
||||
@@ -1730,7 +1871,17 @@
|
||||
"tags": "kommentare,hervorheben,notizen,markieren,entfernen",
|
||||
"title": "Kommentare entfernen",
|
||||
"header": "Kommentare entfernen",
|
||||
"submit": "Entfernen"
|
||||
"submit": "Entfernen",
|
||||
"error": {
|
||||
"failed": "Beim Entfernen der Anmerkungen aus der PDF ist ein Fehler aufgetreten."
|
||||
},
|
||||
"info": {
|
||||
"description": "Dieses Werkzeug entfernt alle Anmerkungen (Kommentare, Hervorhebungen, Notizen usw.) aus Ihren PDF-Dokumenten.",
|
||||
"title": "Über Anmerkungen entfernen"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differenzieren,kontrastieren,verändern,analysieren",
|
||||
@@ -2015,7 +2166,9 @@
|
||||
},
|
||||
"pageSize": {
|
||||
"label": "Ziel-Seitengröße",
|
||||
"keep": "Ursprüngliche Größe beibehalten"
|
||||
"keep": "Ursprüngliche Größe beibehalten",
|
||||
"legal": "Legal",
|
||||
"letter": "Letter"
|
||||
},
|
||||
"submit": "Seitenskalierung anpassen",
|
||||
"error": {
|
||||
@@ -2306,7 +2459,8 @@
|
||||
"showLayers": "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzustand zurückzusetzen)",
|
||||
"colourPicker": "Farbwähler",
|
||||
"findCurrentOutlineItem": "Aktuelles Gliederungselement finden",
|
||||
"applyChanges": "Änderungen anwenden"
|
||||
"applyChanges": "Änderungen anwenden",
|
||||
"zoom": "Zoom"
|
||||
}
|
||||
},
|
||||
"tableExtraxt": {
|
||||
@@ -2496,7 +2650,8 @@
|
||||
"magicLinkSent": "Magic Link wurde an {{email}} gesendet! Prüfen Sie Ihre E-Mails und klicken Sie auf den Link zur Anmeldung.",
|
||||
"passwordResetSent": "Passwort-Reset-Link wurde an {{email}} gesendet! Prüfen Sie Ihre E-Mails und folgen Sie den Anweisungen.",
|
||||
"failedToSignIn": "Anmeldung mit {{provider}} fehlgeschlagen: {{message}}",
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}"
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}",
|
||||
"debug": "Debug"
|
||||
},
|
||||
"signup": {
|
||||
"title": "Konto erstellen",
|
||||
@@ -2518,7 +2673,8 @@
|
||||
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||
"checkEmailConfirmation": "Prüfen Sie Ihre E-Mails auf einen Bestätigungslink, um die Registrierung abzuschließen.",
|
||||
"accountCreatedSuccessfully": "Konto erfolgreich erstellt! Sie können sich jetzt anmelden.",
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}"
|
||||
"unexpectedError": "Unerwarteter Fehler: {{message}}",
|
||||
"name": "Name"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF zu einer Seite zusammenfassen",
|
||||
@@ -2961,7 +3117,12 @@
|
||||
"selectedCount": "{{count}} ausgewählt",
|
||||
"download": "Herunterladen",
|
||||
"delete": "Löschen",
|
||||
"unsupported": "Nicht unterstützt"
|
||||
"unsupported": "Nicht unterstützt",
|
||||
"fileFormat": "Format",
|
||||
"fileName": "Name",
|
||||
"fileVersion": "Version",
|
||||
"googleDrive": "Google Drive",
|
||||
"googleDriveShort": "Drive"
|
||||
},
|
||||
"storage": {
|
||||
"temporaryNotice": "Dateien werden temporär in Ihrem Browser gespeichert und können automatisch gelöscht werden",
|
||||
@@ -2992,12 +3153,24 @@
|
||||
"options": {
|
||||
"title": "Bereinigungs-Optionen",
|
||||
"note": "Wählen Sie die Elemente aus, die Sie aus der PDF entfernen möchten. Mindestens eine Option muss ausgewählt werden.",
|
||||
"removeJavaScript": "JavaScript entfernen",
|
||||
"removeEmbeddedFiles": "Eingebettete Dateien entfernen",
|
||||
"removeXMPMetadata": "XMP-Metadaten entfernen",
|
||||
"removeMetadata": "Dokument-Metadaten entfernen",
|
||||
"removeLinks": "Links entfernen",
|
||||
"removeFonts": "Schriftarten entfernen"
|
||||
"removeJavaScript": {
|
||||
"desc": "JavaScript-Aktionen und Skripte aus der PDF entfernen"
|
||||
},
|
||||
"removeEmbeddedFiles": {
|
||||
"desc": "Alle in der PDF eingebetteten Dateien entfernen"
|
||||
},
|
||||
"removeXMPMetadata": {
|
||||
"desc": "XMP-Metadaten aus der PDF entfernen"
|
||||
},
|
||||
"removeMetadata": {
|
||||
"desc": "Dokumentinformations-Metadaten (Titel, Autor usw.) entfernen"
|
||||
},
|
||||
"removeLinks": {
|
||||
"desc": "Externe Links und Launch-Aktionen aus der PDF entfernen"
|
||||
},
|
||||
"removeFonts": {
|
||||
"desc": "Eingebettete Schriftarten aus der PDF entfernen"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3025,7 +3198,8 @@
|
||||
"keyLength": {
|
||||
"label": "Verschlüsselungsschlüssellänge",
|
||||
"40bit": "40-bit (Niedrig)",
|
||||
"256bit": "256-bit (Hoch)"
|
||||
"256bit": "256-bit (Hoch)",
|
||||
"128bit": "128-bit (Standard)"
|
||||
}
|
||||
},
|
||||
"results": {
|
||||
@@ -3264,5 +3438,58 @@
|
||||
},
|
||||
"generateError": "Wir konnten Ihren API-Schlüssel nicht generieren."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "Weitere Dateien hinzufügen...",
|
||||
"attachments": "Anhänge auswählen",
|
||||
"info": "Wählen Sie Dateien aus, die Sie Ihrer PDF anhängen möchten. Diese Dateien werden eingebettet und über das Anhangs-Panel der PDF zugänglich sein.",
|
||||
"placeholder": "Dateien auswählen...",
|
||||
"results": {
|
||||
"title": "Anhangs-Ergebnisse"
|
||||
},
|
||||
"selectFiles": "Dateien zum Anhängen auswählen",
|
||||
"selectedFiles": "Ausgewählte Dateien",
|
||||
"submit": "Anhänge hinzufügen"
|
||||
},
|
||||
"applyAndContinue": "Anwenden & Fortfahren",
|
||||
"discardChanges": "Änderungen verwerfen",
|
||||
"exportAndContinue": "Exportieren & Fortfahren",
|
||||
"keepWorking": "Weiterarbeiten",
|
||||
"logOut": "Abmelden",
|
||||
"replaceColor": {
|
||||
"tags": "Farbe ersetzen,Seitenoperationen,Backend,serverseitig"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "Beim Extrahieren der Bild-Scans ist ein Fehler aufgetreten."
|
||||
},
|
||||
"submit": "Bild-Scans extrahieren",
|
||||
"title": "Extrahierte Bilder",
|
||||
"tooltip": {
|
||||
"headsUp": "Hinweis",
|
||||
"headsUpDesc": "Überlappende Fotos oder Hintergründe, die farblich sehr nah an den Fotos liegen, können die Genauigkeit verringern - versuchen Sie einen helleren oder dunkleren Hintergrund und lassen Sie mehr Platz.",
|
||||
"problem1": "Fotos nicht erkannt → Toleranz auf 30-50 erhöhen",
|
||||
"problem2": "Zu viele Falscherkennungen → Mindestfläche auf 15.000-20.000 erhöhen",
|
||||
"problem3": "Zuschnitte sind zu eng → Randgröße auf 5-10 erhöhen",
|
||||
"problem4": "Geneigte Fotos nicht begradigt → Winkelschwelle auf ~5° senken",
|
||||
"problem5": "Staub-/Rausch-Boxen → Mindest-Konturfläche auf 1000-2000 erhöhen",
|
||||
"quickFixes": "Schnelle Lösungen",
|
||||
"setupTips": "Einrichtungstipps",
|
||||
"tip1": "Verwenden Sie einen einfachen, hellen Hintergrund",
|
||||
"tip2": "Lassen Sie einen kleinen Abstand (≈1 cm) zwischen den Fotos",
|
||||
"tip3": "Scannen Sie mit 300-600 DPI",
|
||||
"tip4": "Reinigen Sie die Scanner-Glasplatte",
|
||||
"title": "Foto-Teiler",
|
||||
"useCase1": "Ganze Album-Seiten in einem Durchgang scannen",
|
||||
"useCase2": "Flachbett-Stapel in separate Dateien aufteilen",
|
||||
"useCase3": "Collagen in einzelne Fotos aufteilen",
|
||||
"useCase4": "Fotos aus Dokumenten extrahieren",
|
||||
"whatThisDoes": "Was dies tut",
|
||||
"whatThisDoesDesc": "Findet und extrahiert automatisch jedes Foto von einer gescannten Seite oder einem zusammengesetzten Bild - kein manuelles Zuschneiden erforderlich.",
|
||||
"whenToUse": "Wann zu verwenden"
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "Allgemeine Geschäftsbedingungen",
|
||||
"unsavedChanges": "Sie haben ungespeicherte Änderungen an Ihrer PDF. Was möchten Sie tun?",
|
||||
"unsavedChangesTitle": "Ungespeicherte Änderungen"
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"unsavedChanges": "You have unsaved changes to your PDF. What would you like to do?",
|
||||
"unsavedChangesTitle": "Unsaved Changes",
|
||||
"keepWorking": "Keep Working",
|
||||
"discardChanges": "Discard Changes",
|
||||
"applyAndContinue": "Apply & Continue",
|
||||
"exportAndContinue": "Export & Continue",
|
||||
"language": {
|
||||
"direction": "ltr"
|
||||
},
|
||||
@@ -10,16 +16,34 @@
|
||||
"selectText": {
|
||||
"1": "Select PDF file:",
|
||||
"2": "Margin Size",
|
||||
"3": "Position",
|
||||
"3": "Position Selection",
|
||||
"4": "Starting Number",
|
||||
"5": "Pages to Number",
|
||||
"6": "Custom Text"
|
||||
"6": "Custom Text Format"
|
||||
},
|
||||
"customTextDesc": "Custom Text",
|
||||
"numberPagesDesc": "Which pages to number, default 'all', also accepts 1-5 or 2,5,9 etc",
|
||||
"customNumberDesc": "Defaults to {n}, also accepts 'Page {n} of {total}', 'Text-{n}', '{filename}-{n}",
|
||||
"submit": "Add Page Numbers"
|
||||
"numberPagesDesc": "e.g., 1,3,5-8 or leave blank for all pages",
|
||||
"customNumberDesc": "e.g., \"Page {n}\" or leave blank for just numbers",
|
||||
"submit": "Add Page Numbers",
|
||||
"configuration": "Configuration",
|
||||
"customize": "Customize Appearance",
|
||||
"pagesAndStarting": "Pages & Starting Number",
|
||||
"positionAndPages": "Position & Pages",
|
||||
"error": {
|
||||
"failed": "Add page numbers operation failed"
|
||||
},
|
||||
"results": {
|
||||
"title": "Page Number Results"
|
||||
},
|
||||
"preview": "Position Selection",
|
||||
"previewDisclaimer": "Preview is approximate. Final output may vary due to PDF font metrics."
|
||||
},
|
||||
"pageSelectionPrompt": "Specify which pages to add numbers to. Examples: \"1,3,5\" for specific pages, \"1-5\" for ranges, \"2n\" for even pages, or leave blank for all pages.",
|
||||
"startingNumberTooltip": "The first number to display. Subsequent pages will increment from this number.",
|
||||
"marginTooltip": "Distance between the page number and the edge of the page.",
|
||||
"fontSizeTooltip": "Size of the page number text in points. Larger numbers create bigger text.",
|
||||
"fontTypeTooltip": "Font family for the page numbers. Choose based on your document style.",
|
||||
"customTextTooltip": "Optional custom format for page numbers. Use {n} as placeholder for the number. Example: \"Page {n}\" will show \"Page 1\", \"Page 2\", etc.",
|
||||
"pdfPrompt": "Select PDF(s)",
|
||||
"multiPdfPrompt": "Select PDFs (2+)",
|
||||
"multiPdfDropPrompt": "Select (or drag & drop) all PDFs you require",
|
||||
@@ -1726,6 +1750,7 @@
|
||||
"add": "Add",
|
||||
"saved": "Saved Signatures",
|
||||
"save": "Save Signature",
|
||||
"applySignatures": "Apply Signatures",
|
||||
"personalSigs": "Personal Signatures",
|
||||
"sharedSigs": "Shared Signatures",
|
||||
"noSavedSigs": "No saved signatures found",
|
||||
@@ -1737,7 +1762,42 @@
|
||||
"previous": "Previous page",
|
||||
"maintainRatio": "Toggle maintain aspect ratio",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo"
|
||||
"redo": "Redo",
|
||||
"submit": "Sign Document",
|
||||
"steps": {
|
||||
"configure": "Configure Signature"
|
||||
},
|
||||
"type": {
|
||||
"title": "Signature Type",
|
||||
"draw": "Draw",
|
||||
"canvas": "Canvas",
|
||||
"image": "Image",
|
||||
"text": "Text"
|
||||
},
|
||||
"draw": {
|
||||
"title": "Draw your signature",
|
||||
"clear": "Clear"
|
||||
},
|
||||
"image": {
|
||||
"label": "Upload signature image",
|
||||
"placeholder": "Select image file",
|
||||
"hint": "Upload a PNG or JPG image of your signature"
|
||||
},
|
||||
"text": {
|
||||
"name": "Signer Name",
|
||||
"placeholder": "Enter your full name"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "How to add signature"
|
||||
},
|
||||
"activate": "Activate Signature Placement",
|
||||
"deactivate": "Stop Placing Signatures",
|
||||
"results": {
|
||||
"title": "Signature Results"
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while signing the PDF."
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Flatten",
|
||||
@@ -1851,7 +1911,17 @@
|
||||
"tags": "comments,highlight,notes,markup,remove",
|
||||
"title": "Remove Annotations",
|
||||
"header": "Remove Annotations",
|
||||
"submit": "Remove"
|
||||
"submit": "Remove",
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
},
|
||||
"info": {
|
||||
"title": "About Remove Annotations",
|
||||
"description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while removing annotations from the PDF."
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differentiate,contrast,changes,analysis",
|
||||
@@ -3369,6 +3439,16 @@
|
||||
"processImagesDesc": "Converts multiple image files into a single PDF document, then applies OCR technology to extract searchable text from the images."
|
||||
}
|
||||
},
|
||||
"viewer": {
|
||||
"firstPage": "First Page",
|
||||
"lastPage": "Last Page",
|
||||
"previousPage": "Previous Page",
|
||||
"nextPage": "Next Page",
|
||||
"zoomIn": "Zoom In",
|
||||
"zoomOut": "Zoom Out",
|
||||
"singlePageView": "Single Page View",
|
||||
"dualPageView": "Dual Page View"
|
||||
},
|
||||
"common": {
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
@@ -3439,4 +3519,4 @@
|
||||
},
|
||||
"termsAndConditions": "Terms & Conditions",
|
||||
"logOut": "Log out"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1243,7 +1243,17 @@
|
||||
"tags": "comments,highlight,notes,markup,remove",
|
||||
"title": "Remove Annotations",
|
||||
"header": "Remove Annotations",
|
||||
"submit": "Remove"
|
||||
"submit": "Remove",
|
||||
"settings": {
|
||||
"title": "Settings"
|
||||
},
|
||||
"info": {
|
||||
"title": "About Remove Annotations",
|
||||
"description": "This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents."
|
||||
},
|
||||
"error": {
|
||||
"failed": "An error occurred while removing annotations from the PDF."
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differentiate,contrast,changes,analysis",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -87,7 +87,10 @@
|
||||
"showStack": "Mostra traccia dello stack",
|
||||
"copyStack": "Copia traccia dello stack",
|
||||
"githubSubmit": "GitHub: apri un ticket",
|
||||
"discordSubmit": "Discord: invia post di supporto"
|
||||
"discordSubmit": "Discord: invia post di supporto",
|
||||
"dismissAllErrors": "Chiudi tutti gli errori",
|
||||
"encryptedPdfMustRemovePassword": "Questo PDF è crittografato o protetto da password. Si prega di sbloccarlo prima di convertire in PDF/A.",
|
||||
"incorrectPasswordProvided": "La password del PDF è errata o non è stata fornita."
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "Avviso"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "Ordinamento:",
|
||||
"multiTool": {
|
||||
"title": "Multifunzione PDF",
|
||||
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine"
|
||||
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine",
|
||||
"tags": "multipli,strumenti"
|
||||
},
|
||||
"merge": {
|
||||
"title": "Unisci",
|
||||
"desc": "Unisci facilmente più PDF in uno."
|
||||
"desc": "Unisci facilmente più PDF in uno.",
|
||||
"tags": "combina,unisci,unifica"
|
||||
},
|
||||
"split": {
|
||||
"title": "Dividi",
|
||||
"desc": "Dividi un singolo PDF in più documenti."
|
||||
"desc": "Dividi un singolo PDF in più documenti.",
|
||||
"tags": "dividi,separa,spezza"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "Ruota",
|
||||
"desc": "Ruota un PDF."
|
||||
"desc": "Ruota un PDF.",
|
||||
"tags": "ruota,capovolgi,orienta"
|
||||
},
|
||||
"convert": {
|
||||
"title": "Converti",
|
||||
"desc": "Converti file tra diversi formati"
|
||||
"desc": "Converti file tra diversi formati",
|
||||
"tags": "trasforma,cambia"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "Organizza",
|
||||
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine."
|
||||
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine.",
|
||||
"tags": "organizza,riordina,riorganizza"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "Aggiungi Immagine",
|
||||
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)"
|
||||
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)",
|
||||
"tags": "inserisci,incorpora,posiziona"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "Aggiungi allegati",
|
||||
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF"
|
||||
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF",
|
||||
"tags": "incorpora,allega,includi"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "Aggiungi Filigrana",
|
||||
"desc": "Aggiungi una filigrana al tuo PDF."
|
||||
"desc": "Aggiungi una filigrana al tuo PDF.",
|
||||
"tags": "timbro,marca,sovrapponi"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "Rimuovi Password",
|
||||
"desc": "Rimuovi la password dal tuo PDF."
|
||||
"desc": "Rimuovi la password dal tuo PDF.",
|
||||
"tags": "sblocca"
|
||||
},
|
||||
"compress": {
|
||||
"title": "Comprimi",
|
||||
"desc": "Comprimi PDF per ridurne le dimensioni."
|
||||
"desc": "Comprimi PDF per ridurne le dimensioni.",
|
||||
"tags": "riduci,comprimi,ottimizza"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "Sblocca moduli PDF",
|
||||
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF."
|
||||
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF.",
|
||||
"tags": "sblocca,abilita,modifica"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "Modifica Proprietà",
|
||||
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF."
|
||||
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.",
|
||||
"tags": "modifica,cambia,aggiorna"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "OCR / Pulisci scansioni",
|
||||
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro."
|
||||
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro.",
|
||||
"tags": "estrai,scansiona"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "Estrai immagini",
|
||||
"desc": "Estrai tutte le immagini da un PDF e salvale come zip."
|
||||
"desc": "Estrai tutte le immagini da un PDF e salvale come zip.",
|
||||
"tags": "estrai,salva,esporta"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "Rileva/Dividi foto scansionate",
|
||||
"desc": "Divide più foto all’interno di una foto/PDF"
|
||||
"desc": "Divide più foto all’interno di una foto/PDF",
|
||||
"tags": "rileva,dividi,foto"
|
||||
},
|
||||
"sign": {
|
||||
"title": "Firma",
|
||||
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine."
|
||||
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine.",
|
||||
"tags": "firma,autografo"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "Appiattisci",
|
||||
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF."
|
||||
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF.",
|
||||
"tags": "semplifica,rimuovi,interattivo"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "Firma con certificato",
|
||||
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)"
|
||||
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)",
|
||||
"tags": "autentica,PEM,P12,ufficiale,cripta,firma,certificato,PKCS12,JKS,server,manuale,auto"
|
||||
},
|
||||
"repair": {
|
||||
"title": "Ripara",
|
||||
"desc": "Prova a riparare un PDF corrotto."
|
||||
"desc": "Prova a riparare un PDF corrotto.",
|
||||
"tags": "ripara,ripristina"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "Rimuovi pagine vuote",
|
||||
"desc": "Trova e rimuovi pagine vuote da un PDF."
|
||||
"desc": "Trova e rimuovi pagine vuote da un PDF.",
|
||||
"tags": "elimina,pulisci,vuote"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "Rimuovi annotazioni",
|
||||
"desc": "Rimuove tutti i commenti/annotazioni da un PDF"
|
||||
"desc": "Rimuove tutti i commenti/annotazioni da un PDF",
|
||||
"tags": "elimina,pulisci,rimuovi"
|
||||
},
|
||||
"compare": {
|
||||
"title": "Compara",
|
||||
"desc": "Vedi e compara le differenze tra due PDF."
|
||||
"desc": "Vedi e compara le differenze tra due PDF.",
|
||||
"tags": "differenza"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "Rimuovere firma dal certificato",
|
||||
"desc": "Rimuovi la firma del certificato dal PDF"
|
||||
"desc": "Rimuovi la firma del certificato dal PDF",
|
||||
"tags": "rimuovi,elimina,sblocca"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "Layout multipagina",
|
||||
"desc": "Unisci più pagine di un documento PDF in un'unica pagina"
|
||||
"desc": "Unisci più pagine di un documento PDF in un'unica pagina",
|
||||
"tags": "layout,disponi,combina"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "Imposizione a libretto",
|
||||
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura"
|
||||
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura",
|
||||
"tags": "opuscolo,stampa,rilegatura"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "Regola le dimensioni/scala della pagina",
|
||||
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti."
|
||||
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti.",
|
||||
"tags": "ridimensiona,adatta,scala"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "Aggiungi numeri di pagina",
|
||||
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita"
|
||||
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita",
|
||||
"tags": "numero,paginazione,conteggio"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "Rinomina automatica file PDF",
|
||||
"desc": "Rinomina automaticamente un file PDF in base all’intestazione rilevata"
|
||||
"desc": "Rinomina automaticamente un file PDF in base all’intestazione rilevata",
|
||||
"tags": "auto-rilevamento,basato su intestazione,organizza,rinomina"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "Regola colori/contrasto",
|
||||
"desc": "Regola contrasto, saturazione e luminosità di un PDF"
|
||||
"desc": "Regola contrasto, saturazione e luminosità di un PDF",
|
||||
"tags": "contrasto,luminosità,saturazione"
|
||||
},
|
||||
"crop": {
|
||||
"title": "Ritaglia PDF",
|
||||
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)"
|
||||
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)",
|
||||
"tags": "ritaglia,taglia,ridimensiona"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "Pagine divise automaticamente",
|
||||
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato"
|
||||
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato",
|
||||
"tags": "auto,dividi,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "Sanitizza",
|
||||
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF"
|
||||
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF",
|
||||
"tags": "pulisci,elimina,rimuovi"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "Ottieni TUTTE le informazioni in PDF",
|
||||
"desc": "Raccogli tutte le informazioni possibili sui PDF"
|
||||
"desc": "Raccogli tutte le informazioni possibili sui PDF",
|
||||
"tags": "info,metadati,dettagli"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF in un'unica pagina di grandi dimensioni",
|
||||
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina"
|
||||
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina",
|
||||
"tags": "combina,unisci,singola"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "Mostra Javascript",
|
||||
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF"
|
||||
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF",
|
||||
"tags": "javascript,codice,script"
|
||||
},
|
||||
"redact": {
|
||||
"title": "Redazione manuale",
|
||||
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)"
|
||||
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)",
|
||||
"tags": "censura,oscura,nascondi"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "Sovrapponi PDF",
|
||||
"desc": "Sovrapponi PDF sopra un altro PDF"
|
||||
"desc": "Sovrapponi PDF sopra un altro PDF",
|
||||
"tags": "sovrapponi,combina,impila"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "Dividi PDF per sezioni",
|
||||
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole"
|
||||
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole",
|
||||
"tags": "dividi,sezioni,separa"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "Aggiungi timbro al PDF",
|
||||
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche"
|
||||
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche",
|
||||
"tags": "timbro,marca,sigillo"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "Rimuovi immagine",
|
||||
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file"
|
||||
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file",
|
||||
"tags": "rimuovi,elimina,pulisci"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "Dividi PDF per capitoli",
|
||||
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli."
|
||||
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli.",
|
||||
"tags": "dividi,capitoli,struttura"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "Convalida la firma PDF",
|
||||
"desc": "Verificare le firme digitali e i certificati nei documenti PDF"
|
||||
"desc": "Verificare le firme digitali e i certificati nei documenti PDF",
|
||||
"tags": "convalida,verifica,certificato"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "Documentazione API",
|
||||
"desc": "Visualizza documentazione API e testa gli endpoint"
|
||||
"desc": "Visualizza documentazione API e testa gli endpoint",
|
||||
"tags": "API,documentazione,test"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "Finta scansione",
|
||||
@@ -538,31 +585,38 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "Modifica indice",
|
||||
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF"
|
||||
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF",
|
||||
"tags": "segnalibri,contenuti,modifica"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "Gestisci certificati",
|
||||
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF."
|
||||
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF.",
|
||||
"tags": "certificati,importa,esporta"
|
||||
},
|
||||
"read": {
|
||||
"title": "Leggi",
|
||||
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione."
|
||||
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione.",
|
||||
"tags": "visualizza,apri,mostra"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "Riorganizza pagine",
|
||||
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo drag‑and‑drop."
|
||||
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo drag‑and‑drop.",
|
||||
"tags": "riordina,riorganizza,organizza"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "Estrai pagine",
|
||||
"desc": "Estrai pagine specifiche da un PDF"
|
||||
"desc": "Estrai pagine specifiche da un PDF",
|
||||
"tags": "estrai,seleziona,copia"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "Rimuovi",
|
||||
"desc": "Elimina alcune pagine dal PDF."
|
||||
"desc": "Elimina alcune pagine dal PDF.",
|
||||
"tags": "elimina,estrai,escludi"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "Divisione automatica per dimensione/numero",
|
||||
"desc": "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti"
|
||||
"desc": "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti",
|
||||
"tags": "auto,dividi,dimensione"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "Sostituisci e inverti il colore",
|
||||
@@ -570,11 +624,13 @@
|
||||
},
|
||||
"devApi": {
|
||||
"title": "API",
|
||||
"desc": "Link alla documentazione API"
|
||||
"desc": "Link alla documentazione API",
|
||||
"tags": "API,sviluppo,documentazione"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "Scansione cartelle automatizzata",
|
||||
"desc": "Link alla guida per scansione cartelle automatizzata"
|
||||
"desc": "Link alla guida per scansione cartelle automatizzata",
|
||||
"tags": "automazione,cartella,scansione"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "Guida SSO",
|
||||
@@ -594,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "Automatizza",
|
||||
"desc": "Crea flussi multi‑step concatenando azioni PDF. Ideale per attività ricorrenti."
|
||||
"desc": "Crea flussi multi‑step concatenando azioni PDF. Ideale per attività ricorrenti.",
|
||||
"tags": "flusso di lavoro,sequenza,automazione"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "Sostituisci o inverti i colori nei documenti PDF",
|
||||
"title": "Sostituisci e inverti colore"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "Crea un PDF che sembra essere stato scansionato",
|
||||
"tags": "scansiona,simula,crea",
|
||||
"title": "Effetto scanner"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -654,7 +720,9 @@
|
||||
},
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l’unione dei PDF."
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": "Generare l'indice nel file unito?",
|
||||
"removeDigitalSignature": "Rimuovere la firma digitale nel file unito?"
|
||||
},
|
||||
"split": {
|
||||
"tags": "Operazioni sulla pagina,divisione,multi pagina,taglio,lato server",
|
||||
@@ -913,7 +981,20 @@
|
||||
"10": "Unione pari-dispari",
|
||||
"11": "Duplica tutte le pagine"
|
||||
},
|
||||
"placeholder": "(ad es. 1,3,2 o 4-8,2,10-12 o 2n-1)"
|
||||
"placeholder": "(ad es. 1,3,2 o 4-8,2,10-12 o 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "Disporre le pagine per la stampa a opuscolo (ultima, prima, seconda, penultima, …).",
|
||||
"CUSTOM": "Utilizzare una sequenza personalizzata di numeri di pagina o espressioni per definire un nuovo ordine.",
|
||||
"DUPLEX_SORT": "Alternare fronte e retro come se uno scanner duplex avesse scansionato tutti i fronti, poi tutti i retri (1, n, 2, n-1, …).",
|
||||
"DUPLICATE": "Duplicare ogni pagina secondo il conteggio dell'ordine personalizzato (ad es., 4 duplica ogni pagina 4×).",
|
||||
"ODD_EVEN_MERGE": "Unire due PDF alternando le pagine: dispari dal primo, pari dal secondo.",
|
||||
"ODD_EVEN_SPLIT": "Dividere il documento in due output: tutte le pagine dispari e tutte le pagine pari.",
|
||||
"REMOVE_FIRST": "Rimuovere la prima pagina dal documento.",
|
||||
"REMOVE_FIRST_AND_LAST": "Rimuovere sia la prima che l'ultima pagina dal documento.",
|
||||
"REMOVE_LAST": "Rimuovere l'ultima pagina dal documento.",
|
||||
"REVERSE_ORDER": "Capovolgere il documento in modo che l'ultima pagina diventi la prima e così via.",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "Disporre le pagine per la stampa a opuscolo con cucitura laterale (ottimizzato per la rilegatura sul lato)."
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "img,jpg,immagine,foto",
|
||||
@@ -1347,7 +1428,7 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "Stato Trapped",
|
||||
"unknown": "Unknown",
|
||||
"unknown": "Sconosciuto",
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
@@ -1538,7 +1619,13 @@
|
||||
"header": "Estrai immagini",
|
||||
"selectText": "Seleziona il formato in cui salvare le immagini estratte",
|
||||
"allowDuplicates": "Salva le immagini duplicate",
|
||||
"submit": "Estrai"
|
||||
"submit": "Estrai",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l'estrazione delle immagini dal PDF."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "archivio,a lungo termine,standard,conversione,archiviazione,conservazione",
|
||||
@@ -1615,8 +1702,14 @@
|
||||
"title": "Firma",
|
||||
"header": "Firma PDF",
|
||||
"upload": "Carica immagine",
|
||||
"draw": "Disegna Firma",
|
||||
"text": "Testo",
|
||||
"draw": {
|
||||
"clear": "Cancella",
|
||||
"title": "Disegna la tua firma"
|
||||
},
|
||||
"text": {
|
||||
"name": "Nome firmatario",
|
||||
"placeholder": "Inserisci il tuo nome completo"
|
||||
},
|
||||
"clear": "Cancella",
|
||||
"add": "Aggiungi",
|
||||
"saved": "Firme salvate",
|
||||
@@ -1632,7 +1725,35 @@
|
||||
"previous": "Pagina precedente",
|
||||
"maintainRatio": "Attiva il mantenimento delle proporzioni",
|
||||
"undo": "Annulla",
|
||||
"redo": "Rifare"
|
||||
"redo": "Rifare",
|
||||
"activate": "Attiva posizionamento firma",
|
||||
"applySignatures": "Applica firme",
|
||||
"deactivate": "Interrompi posizionamento firme",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante la firma del PDF."
|
||||
},
|
||||
"image": {
|
||||
"hint": "Carica un'immagine PNG o JPG della tua firma",
|
||||
"label": "Carica immagine firma",
|
||||
"placeholder": "Seleziona file immagine"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "Come aggiungere la firma"
|
||||
},
|
||||
"results": {
|
||||
"title": "Risultati firma"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "Configura firma"
|
||||
},
|
||||
"submit": "Firma documento",
|
||||
"type": {
|
||||
"canvas": "Canvas",
|
||||
"draw": "Disegna",
|
||||
"image": "Immagine",
|
||||
"text": "Testo",
|
||||
"title": "Tipo di firma"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "statico,disattivato,non interattivo,ottimizzato",
|
||||
@@ -1651,7 +1772,8 @@
|
||||
"stepTitle": "Opzioni di flattening",
|
||||
"title": "Opzioni di flattening",
|
||||
"flattenOnlyForms.desc": "Appiattisci solo i campi modulo, lasciando intatti gli altri elementi interattivi",
|
||||
"note": "Il flattening rimuove gli elementi interattivi dal PDF, rendendoli non modificabili."
|
||||
"note": "Il flattening rimuove gli elementi interattivi dal PDF, rendendoli non modificabili.",
|
||||
"flattenOnlyForms": "Appiattisci solo i moduli"
|
||||
},
|
||||
"results": {
|
||||
"title": "Risultati Flatten"
|
||||
@@ -1747,7 +1869,17 @@
|
||||
"tags": "commenti,evidenziazioni,note,markup,rimozione",
|
||||
"title": "Rimuovi Annotazioni",
|
||||
"header": "Rimuovi Annotazioni",
|
||||
"submit": "Rimuovi"
|
||||
"submit": "Rimuovi",
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante la rimozione delle annotazioni dal PDF."
|
||||
},
|
||||
"info": {
|
||||
"description": "Questo strumento rimuoverà tutte le annotazioni (commenti, evidenziazioni, note, ecc.) dai tuoi documenti PDF.",
|
||||
"title": "Informazioni su Rimuovi annotazioni"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "differenziare,contrastare,cambiare,analisi",
|
||||
@@ -3024,7 +3156,13 @@
|
||||
"removeXMPMetadata.desc": "Rimuovi i metadati XMP dal PDF",
|
||||
"removeMetadata.desc": "Rimuovi le informazioni (titolo, autore, ecc.)",
|
||||
"removeLinks.desc": "Rimuovi link esterni e azioni di avvio dal PDF",
|
||||
"removeFonts.desc": "Rimuovi i font incorporati dal PDF"
|
||||
"removeFonts.desc": "Rimuovi i font incorporati dal PDF",
|
||||
"removeEmbeddedFiles": "Rimuovi file incorporati",
|
||||
"removeFonts": "Rimuovi caratteri",
|
||||
"removeJavaScript": "Rimuovi JavaScript",
|
||||
"removeLinks": "Rimuovi collegamenti",
|
||||
"removeMetadata": "Rimuovi metadati documento",
|
||||
"removeXMPMetadata": "Rimuovi metadati XMP"
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3294,5 +3432,56 @@
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "Termini e condizioni",
|
||||
"logOut": "Esci"
|
||||
"logOut": "Esci",
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "Aggiungi altri file...",
|
||||
"attachments": "Seleziona allegati",
|
||||
"info": "Seleziona i file da allegare al tuo PDF. Questi file saranno incorporati e accessibili tramite il pannello allegati del PDF.",
|
||||
"placeholder": "Scegli file...",
|
||||
"results": {
|
||||
"title": "Risultati allegati"
|
||||
},
|
||||
"selectFiles": "Seleziona file da allegare",
|
||||
"selectedFiles": "File selezionati",
|
||||
"submit": "Aggiungi allegati"
|
||||
},
|
||||
"applyAndContinue": "Applica e continua",
|
||||
"discardChanges": "Scarta modifiche",
|
||||
"exportAndContinue": "Esporta e continua",
|
||||
"keepWorking": "Continua a lavorare",
|
||||
"replaceColor": {
|
||||
"tags": "Sostituisci colore,Operazioni pagina,Back end,lato server"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "Si è verificato un errore durante l'estrazione delle scansioni di immagini."
|
||||
},
|
||||
"submit": "Estrai scansioni di immagini",
|
||||
"title": "Immagini estratte",
|
||||
"tooltip": {
|
||||
"headsUp": "Attenzione",
|
||||
"headsUpDesc": "Foto sovrapposte o sfondi molto simili nel colore alle foto possono ridurre la precisione - prova uno sfondo più chiaro o più scuro e lascia più spazio.",
|
||||
"problem1": "Foto non rilevate → aumentare la tolleranza a 30-50",
|
||||
"problem2": "Troppe rilevazioni errate → aumentare l'area minima a 15.000-20.000",
|
||||
"problem3": "I ritagli sono troppo stretti → aumentare la dimensione del bordo a 5-10",
|
||||
"problem4": "Foto inclinate non raddrizzate → abbassare la soglia angolare a ~5°",
|
||||
"problem5": "Caselle di polvere/rumore → aumentare l'area minima del contorno a 1000-2000",
|
||||
"quickFixes": "Correzioni rapide",
|
||||
"setupTips": "Suggerimenti di configurazione",
|
||||
"tip1": "Usa uno sfondo semplice e chiaro",
|
||||
"tip2": "Lascia un piccolo spazio (≈1 cm) tra le foto",
|
||||
"tip3": "Scansiona a 300-600 DPI",
|
||||
"tip4": "Pulisci il vetro dello scanner",
|
||||
"title": "Divisore di foto",
|
||||
"useCase1": "Scansiona intere pagine di album in una volta",
|
||||
"useCase2": "Dividi i lotti flatbed in file separati",
|
||||
"useCase3": "Suddividi collage in singole foto",
|
||||
"useCase4": "Estrai foto dai documenti",
|
||||
"whatThisDoes": "Cosa fa",
|
||||
"whatThisDoesDesc": "Trova ed estrae automaticamente ogni foto da una pagina scansionata o da un'immagine composita - senza ritaglio manuale.",
|
||||
"whenToUse": "Quando usare"
|
||||
}
|
||||
},
|
||||
"unsavedChanges": "Hai modifiche non salvate al tuo PDF. Cosa vuoi fare?",
|
||||
"unsavedChangesTitle": "Modifiche non salvate"
|
||||
}
|
||||
@@ -87,7 +87,10 @@
|
||||
"showStack": "显示堆栈跟踪",
|
||||
"copyStack": "复制堆栈跟踪",
|
||||
"githubSubmit": "GitHub - 提交工单",
|
||||
"discordSubmit": "Discord - 提交支持帖子"
|
||||
"discordSubmit": "Discord - 提交支持帖子",
|
||||
"dismissAllErrors": "关闭所有错误",
|
||||
"encryptedPdfMustRemovePassword": "此 PDF 已加密或受密码保护。请在转换为 PDF/A 之前将其解锁。",
|
||||
"incorrectPasswordProvided": "PDF 密码不正确或未提供。"
|
||||
},
|
||||
"warning": {
|
||||
"tooltipTitle": "警告"
|
||||
@@ -358,179 +361,223 @@
|
||||
"sortBy": "排序:",
|
||||
"multiTool": {
|
||||
"title": "PDF 多功能工具",
|
||||
"desc": "合并、旋转、重新排列和删除 PDF 页面"
|
||||
"desc": "合并、旋转、重新排列和删除 PDF 页面",
|
||||
"tags": "多个,工具"
|
||||
},
|
||||
"merge": {
|
||||
"title": "合并",
|
||||
"desc": "轻松将多个 PDF 合并成一个。"
|
||||
"desc": "轻松将多个 PDF 合并成一个。",
|
||||
"tags": "组合,合并,联合"
|
||||
},
|
||||
"split": {
|
||||
"title": "拆分",
|
||||
"desc": "将 PDF 拆分为多个文档。"
|
||||
"desc": "将 PDF 拆分为多个文档。",
|
||||
"tags": "分割,分离,拆分"
|
||||
},
|
||||
"rotate": {
|
||||
"title": "旋转",
|
||||
"desc": "旋转 PDF。"
|
||||
"desc": "旋转 PDF。",
|
||||
"tags": "旋转,翻转,定向"
|
||||
},
|
||||
"convert": {
|
||||
"title": "转换",
|
||||
"desc": "在不同格式之间转换文件"
|
||||
"desc": "在不同格式之间转换文件",
|
||||
"tags": "转换,更改"
|
||||
},
|
||||
"pdfOrganiser": {
|
||||
"title": "整理",
|
||||
"desc": "按任意顺序删除/重新排列页面。"
|
||||
"desc": "按任意顺序删除/重新排列页面。",
|
||||
"tags": "组织,重新排列,重新排序"
|
||||
},
|
||||
"addImage": {
|
||||
"title": "在 PDF 中添加图片",
|
||||
"desc": "将图像添加到 PDF 的指定位置。"
|
||||
"desc": "将图像添加到 PDF 的指定位置。",
|
||||
"tags": "插入,嵌入,放置"
|
||||
},
|
||||
"addAttachments": {
|
||||
"title": "添加附件",
|
||||
"desc": "向 PDF 添加或移除嵌入文件(附件)"
|
||||
"desc": "向 PDF 添加或移除嵌入文件(附件)",
|
||||
"tags": "嵌入,附加,包含"
|
||||
},
|
||||
"watermark": {
|
||||
"title": "添加水印",
|
||||
"desc": "在 PDF 中添加自定义水印。"
|
||||
"desc": "在 PDF 中添加自定义水印。",
|
||||
"tags": "印章,标记,叠加"
|
||||
},
|
||||
"removePassword": {
|
||||
"title": "删除密码",
|
||||
"desc": "从 PDF 文档中移除密码保护。"
|
||||
"desc": "从 PDF 文档中移除密码保护。",
|
||||
"tags": "解锁"
|
||||
},
|
||||
"compress": {
|
||||
"title": "压缩",
|
||||
"desc": "压缩 PDF 文件以减小文件大小。"
|
||||
"desc": "压缩 PDF 文件以减小文件大小。",
|
||||
"tags": "缩小,减少,优化"
|
||||
},
|
||||
"unlockPDFForms": {
|
||||
"title": "解锁PDF表单",
|
||||
"desc": "移除表单字段只读属性"
|
||||
"desc": "移除表单字段只读属性",
|
||||
"tags": "解锁,启用,编辑"
|
||||
},
|
||||
"changeMetadata": {
|
||||
"title": "更改元数据",
|
||||
"desc": "更改/删除/添加 PDF 文档的元数据。"
|
||||
"desc": "更改/删除/添加 PDF 文档的元数据。",
|
||||
"tags": "编辑,修改,更新"
|
||||
},
|
||||
"ocr": {
|
||||
"title": "运行 OCR /清理扫描",
|
||||
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。"
|
||||
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。",
|
||||
"tags": "提取,扫描"
|
||||
},
|
||||
"extractImages": {
|
||||
"title": "提取图像",
|
||||
"desc": "从 PDF 中提取所有图像并保存到压缩包中。"
|
||||
"desc": "从 PDF 中提取所有图像并保存到压缩包中。",
|
||||
"tags": "提取,保存,导出"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"title": "检测/拆分扫描照片",
|
||||
"desc": "从照片/PDF 中拆分出多张照片"
|
||||
"desc": "从照片/PDF 中拆分出多张照片",
|
||||
"tags": "检测,拆分,照片"
|
||||
},
|
||||
"sign": {
|
||||
"title": "签名",
|
||||
"desc": "通过绘图、文字或图像向 PDF 添加签名"
|
||||
"desc": "通过绘图、文字或图像向 PDF 添加签名",
|
||||
"tags": "签名,亲笔签名"
|
||||
},
|
||||
"flatten": {
|
||||
"title": "展平",
|
||||
"desc": "从 PDF 中删除所有互动元素和表单"
|
||||
"desc": "从 PDF 中删除所有互动元素和表单",
|
||||
"tags": "简化,删除,交互式"
|
||||
},
|
||||
"certSign": {
|
||||
"title": "使用证书签名",
|
||||
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名"
|
||||
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名",
|
||||
"tags": "认证,PEM,P12,官方,加密,签名,证书,PKCS12,JKS,服务器,手动,自动"
|
||||
},
|
||||
"repair": {
|
||||
"title": "修复",
|
||||
"desc": "尝试修复损坏/损坏的 PDF"
|
||||
"desc": "尝试修复损坏/损坏的 PDF",
|
||||
"tags": "修复,恢复"
|
||||
},
|
||||
"removeBlanks": {
|
||||
"title": "删除空白页",
|
||||
"desc": "检测并删除文档中的空白页"
|
||||
"desc": "检测并删除文档中的空白页",
|
||||
"tags": "删除,清理,空白"
|
||||
},
|
||||
"removeAnnotations": {
|
||||
"title": "删除标注",
|
||||
"desc": "删除 PDF 中的所有标注/评论"
|
||||
"desc": "删除 PDF 中的所有标注/评论",
|
||||
"tags": "删除,清理,删除"
|
||||
},
|
||||
"compare": {
|
||||
"title": "比较",
|
||||
"desc": "比较并显示两个 PDF 文档之间的差异"
|
||||
"desc": "比较并显示两个 PDF 文档之间的差异",
|
||||
"tags": "差异"
|
||||
},
|
||||
"removeCertSign": {
|
||||
"title": "移除证书签名",
|
||||
"desc": "移除 PDF 的证书签名"
|
||||
"desc": "移除 PDF 的证书签名",
|
||||
"tags": "删除,删除,解锁"
|
||||
},
|
||||
"pageLayout": {
|
||||
"title": "多页布局",
|
||||
"desc": "将 PDF 文档的多个页面合并成一页"
|
||||
"desc": "将 PDF 文档的多个页面合并成一页",
|
||||
"tags": "布局,排列,组合"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"title": "小册子拼版",
|
||||
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订"
|
||||
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订",
|
||||
"tags": "小册子,打印,装订"
|
||||
},
|
||||
"scalePages": {
|
||||
"title": "调整页面尺寸/缩放",
|
||||
"desc": "调整页面及/或其内容的尺寸/缩放"
|
||||
"desc": "调整页面及/或其内容的尺寸/缩放",
|
||||
"tags": "调整大小,调整,缩放"
|
||||
},
|
||||
"addPageNumbers": {
|
||||
"title": "添加页码",
|
||||
"desc": "在文档的指定位置添加页码"
|
||||
"desc": "在文档的指定位置添加页码",
|
||||
"tags": "编号,分页,计数"
|
||||
},
|
||||
"autoRename": {
|
||||
"title": "自动重命名 PDF 文件",
|
||||
"desc": "基于检测到的页眉自动重命名 PDF 文件"
|
||||
"desc": "基于检测到的页眉自动重命名 PDF 文件",
|
||||
"tags": "自动检测,基于标题,组织,重新标记"
|
||||
},
|
||||
"adjustContrast": {
|
||||
"title": "调整颜色/对比度",
|
||||
"desc": "调整 PDF 的对比度、饱和度和亮度"
|
||||
"desc": "调整 PDF 的对比度、饱和度和亮度",
|
||||
"tags": "对比度,亮度,饱和度"
|
||||
},
|
||||
"crop": {
|
||||
"title": "裁剪 PDF",
|
||||
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)"
|
||||
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)",
|
||||
"tags": "裁剪,剪切,调整大小"
|
||||
},
|
||||
"autoSplitPDF": {
|
||||
"title": "自动拆分页面",
|
||||
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF"
|
||||
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF",
|
||||
"tags": "自动,拆分,QR"
|
||||
},
|
||||
"sanitize": {
|
||||
"title": "安全清理",
|
||||
"desc": "移除 PDF 文件中的潜在有害元素"
|
||||
"desc": "移除 PDF 文件中的潜在有害元素",
|
||||
"tags": "清理,清除,删除"
|
||||
},
|
||||
"getPdfInfo": {
|
||||
"title": "获取 PDF 的所有信息",
|
||||
"desc": "获取 PDF 的所有可能的信息"
|
||||
"desc": "获取 PDF 的所有可能的信息",
|
||||
"tags": "信息,元数据,详细信息"
|
||||
},
|
||||
"pdfToSinglePage": {
|
||||
"title": "PDF 转单一大页",
|
||||
"desc": "将所有 PDF 页面合并为一个大的单页"
|
||||
"desc": "将所有 PDF 页面合并为一个大的单页",
|
||||
"tags": "组合,合并,单页"
|
||||
},
|
||||
"showJS": {
|
||||
"title": "显示 JavaScript",
|
||||
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码"
|
||||
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码",
|
||||
"tags": "javascript,代码,脚本"
|
||||
},
|
||||
"redact": {
|
||||
"title": "手动修订",
|
||||
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF"
|
||||
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF",
|
||||
"tags": "审查,涂黑,隐藏"
|
||||
},
|
||||
"overlayPdfs": {
|
||||
"title": "叠加 PDF",
|
||||
"desc": "将一个 PDF 叠加到另一个 PDF 之上"
|
||||
"desc": "将一个 PDF 叠加到另一个 PDF 之上",
|
||||
"tags": "叠加,组合,堆叠"
|
||||
},
|
||||
"splitBySections": {
|
||||
"title": "按区块拆分 PDF",
|
||||
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块"
|
||||
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块",
|
||||
"tags": "拆分,部分,分割"
|
||||
},
|
||||
"addStamp": {
|
||||
"title": "向 PDF 添加印章",
|
||||
"desc": "在指定位置添加文本或图像印章"
|
||||
"desc": "在指定位置添加文本或图像印章",
|
||||
"tags": "印章,标记,盖章"
|
||||
},
|
||||
"removeImage": {
|
||||
"title": "删除图像",
|
||||
"desc": "删除图像减少 PDF 大小"
|
||||
"desc": "删除图像减少 PDF 大小",
|
||||
"tags": "删除,删除,清理"
|
||||
},
|
||||
"splitByChapters": {
|
||||
"title": "按章节拆分 PDF",
|
||||
"desc": "根据其章节结构将 PDF 拆分为多个文件。"
|
||||
"desc": "根据其章节结构将 PDF 拆分为多个文件。",
|
||||
"tags": "拆分,章节,结构"
|
||||
},
|
||||
"validateSignature": {
|
||||
"title": "验证 PDF 签名",
|
||||
"desc": "验证 PDF 文档中的数字签名和证书"
|
||||
"desc": "验证 PDF 文档中的数字签名和证书",
|
||||
"tags": "验证,核实,证书"
|
||||
},
|
||||
"swagger": {
|
||||
"title": "API 文档",
|
||||
"desc": "查看 API 文档并测试端点"
|
||||
"desc": "查看 API 文档并测试端点",
|
||||
"tags": "API,文档,测试"
|
||||
},
|
||||
"fakeScan": {
|
||||
"title": "伪扫描",
|
||||
@@ -538,31 +585,38 @@
|
||||
},
|
||||
"editTableOfContents": {
|
||||
"title": "编辑目录",
|
||||
"desc": "为 PDF 文档添加或编辑目录和书签"
|
||||
"desc": "为 PDF 文档添加或编辑目录和书签",
|
||||
"tags": "书签,目录,编辑"
|
||||
},
|
||||
"manageCertificates": {
|
||||
"title": "管理证书",
|
||||
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。"
|
||||
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。",
|
||||
"tags": "证书,导入,导出"
|
||||
},
|
||||
"read": {
|
||||
"title": "阅读",
|
||||
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。"
|
||||
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。",
|
||||
"tags": "查看,打开,显示"
|
||||
},
|
||||
"reorganizePages": {
|
||||
"title": "重组页面",
|
||||
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。"
|
||||
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。",
|
||||
"tags": "重新排列,重新排序,组织"
|
||||
},
|
||||
"extractPages": {
|
||||
"title": "提取页面",
|
||||
"desc": "从 PDF 文档中提取特定页面"
|
||||
"desc": "从 PDF 文档中提取特定页面",
|
||||
"tags": "提取,选择,复制"
|
||||
},
|
||||
"removePages": {
|
||||
"title": "删除",
|
||||
"desc": "从 PDF 文档中删除不需要的页面。"
|
||||
"desc": "从 PDF 文档中删除不需要的页面。",
|
||||
"tags": "删除,提取,排除"
|
||||
},
|
||||
"autoSizeSplitPDF": {
|
||||
"title": "自动根据大小/数目拆分 PDF",
|
||||
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数"
|
||||
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数",
|
||||
"tags": "自动,拆分,大小"
|
||||
},
|
||||
"replaceColorPdf": {
|
||||
"title": "替换和反转颜色",
|
||||
@@ -570,11 +624,13 @@
|
||||
},
|
||||
"devApi": {
|
||||
"title": "API",
|
||||
"desc": "跳转至 API 文档"
|
||||
"desc": "跳转至 API 文档",
|
||||
"tags": "API,开发,文档"
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"title": "自动文件夹扫描",
|
||||
"desc": "跳转至自动文件夹扫描指南"
|
||||
"desc": "跳转至自动文件夹扫描指南",
|
||||
"tags": "自动化,文件夹,扫描"
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"title": "SSO 指南",
|
||||
@@ -594,7 +650,17 @@
|
||||
},
|
||||
"automate": {
|
||||
"title": "自动化",
|
||||
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。"
|
||||
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。",
|
||||
"tags": "工作流,序列,自动化"
|
||||
},
|
||||
"replaceColor": {
|
||||
"desc": "替换或反转 PDF 文档中的颜色",
|
||||
"title": "替换和反转颜色"
|
||||
},
|
||||
"scannerEffect": {
|
||||
"desc": "创建看起来像扫描的 PDF",
|
||||
"tags": "扫描,模拟,创建",
|
||||
"title": "扫描仪效果"
|
||||
}
|
||||
},
|
||||
"landing": {
|
||||
@@ -654,7 +720,9 @@
|
||||
},
|
||||
"error": {
|
||||
"failed": "合并 PDF 时发生错误。"
|
||||
}
|
||||
},
|
||||
"generateTableOfContents": "在合并的文件中生成目录?",
|
||||
"removeDigitalSignature": "在合并的文件中删除数字签名?"
|
||||
},
|
||||
"split": {
|
||||
"tags": "页面操作,划分,多页面,剪切,服务器端",
|
||||
@@ -815,7 +883,7 @@
|
||||
"settings": "设置",
|
||||
"conversionCompleted": "转换完成",
|
||||
"results": "结果",
|
||||
"defaultFilename": "converted_file",
|
||||
"defaultFilename": "已转换文件",
|
||||
"conversionResults": "转换结果",
|
||||
"convertFrom": "转换来源",
|
||||
"convertTo": "转换为",
|
||||
@@ -913,7 +981,20 @@
|
||||
"10": "奇偶合并",
|
||||
"11": "复制所有页面"
|
||||
},
|
||||
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1)"
|
||||
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1)",
|
||||
"desc": {
|
||||
"BOOKLET_SORT": "排列页面以进行小册子打印(最后,第一,第二,倒数第二,...)。",
|
||||
"CUSTOM": "使用自定义的页码序列或表达式来定义新顺序。",
|
||||
"DUPLEX_SORT": "交错正面然后背面,就像双面扫描仪扫描了所有正面,然后所有背面(1, n, 2, n-1, ...)。",
|
||||
"DUPLICATE": "根据自定义顺序计数复制每页(例如,4 复制每页 4×)。",
|
||||
"ODD_EVEN_MERGE": "通过交替页面合并两个 PDF:第一个的奇数页,第二个的偶数页。",
|
||||
"ODD_EVEN_SPLIT": "将文档拆分为两个输出:所有奇数页和所有偶数页。",
|
||||
"REMOVE_FIRST": "从文档中删除第一页。",
|
||||
"REMOVE_FIRST_AND_LAST": "从文档中删除第一页和最后一页。",
|
||||
"REMOVE_LAST": "从文档中删除最后一页。",
|
||||
"REVERSE_ORDER": "翻转文档,使最后一页变为第一页,依此类推。",
|
||||
"SIDE_STITCH_BOOKLET_SORT": "排列页面以进行侧缝小册子打印(针对侧面装订进行了优化)。"
|
||||
}
|
||||
},
|
||||
"addImage": {
|
||||
"tags": "图像、JPG、图片、照片",
|
||||
@@ -937,7 +1018,7 @@
|
||||
"desc": "向 PDF 添加文本或图像水印",
|
||||
"completed": "已添加水印",
|
||||
"submit": "添加水印",
|
||||
"filenamePrefix": "watermarked",
|
||||
"filenamePrefix": "已加水印",
|
||||
"error": {
|
||||
"failed": "向 PDF 添加水印时发生错误。"
|
||||
},
|
||||
@@ -1136,7 +1217,7 @@
|
||||
"placeholder": "例如:1,3,5-8,10",
|
||||
"error": "无效的页码格式。使用数字、范围(1-5)或数学表达式(2n+1)"
|
||||
},
|
||||
"filenamePrefix": "pages_removed",
|
||||
"filenamePrefix": "已删除页面",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1284,7 +1365,7 @@
|
||||
"header": "解锁 PDF 表单",
|
||||
"submit": "Remove",
|
||||
"description": "该工具将移除 PDF 表单字段的只读限制,使其可编辑、可填写。",
|
||||
"filenamePrefix": "unlocked_forms",
|
||||
"filenamePrefix": "已解锁表单",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1299,7 +1380,7 @@
|
||||
"tags": "标题、作者、日期、创建、时间、发布者、制作人、统计数据",
|
||||
"header": "更改元数据",
|
||||
"submit": "更改",
|
||||
"filenamePrefix": "metadata",
|
||||
"filenamePrefix": "元数据",
|
||||
"settings": {
|
||||
"title": "元数据设置"
|
||||
},
|
||||
@@ -1347,7 +1428,7 @@
|
||||
},
|
||||
"trapped": {
|
||||
"label": "陷印状态",
|
||||
"unknown": "Unknown",
|
||||
"unknown": "未知",
|
||||
"true": "True",
|
||||
"false": "False"
|
||||
},
|
||||
@@ -1538,7 +1619,13 @@
|
||||
"header": "提取图像",
|
||||
"selectText": "选择图像格式,将提取的图像转换为",
|
||||
"allowDuplicates": "保存重复图像",
|
||||
"submit": "提取"
|
||||
"submit": "提取",
|
||||
"error": {
|
||||
"failed": "从 PDF 提取图像时发生错误。"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置"
|
||||
}
|
||||
},
|
||||
"pdfToPDFA": {
|
||||
"tags": "归档、长期、标准、转换、存储、保存",
|
||||
@@ -1615,8 +1702,14 @@
|
||||
"title": "签名",
|
||||
"header": "签署 PDF",
|
||||
"upload": "上传图片",
|
||||
"draw": "绘制签名",
|
||||
"text": "文本输入",
|
||||
"draw": {
|
||||
"clear": "清除",
|
||||
"title": "绘制您的签名"
|
||||
},
|
||||
"text": {
|
||||
"name": "签署人姓名",
|
||||
"placeholder": "输入您的全名"
|
||||
},
|
||||
"clear": "清除",
|
||||
"add": "添加",
|
||||
"saved": "已保存签名",
|
||||
@@ -1632,7 +1725,35 @@
|
||||
"previous": "上一页",
|
||||
"maintainRatio": "切换保持长宽比",
|
||||
"undo": "撤销",
|
||||
"redo": "重做"
|
||||
"redo": "重做",
|
||||
"activate": "激活签名放置",
|
||||
"applySignatures": "应用签名",
|
||||
"deactivate": "停止放置签名",
|
||||
"error": {
|
||||
"failed": "签署 PDF 时发生错误。"
|
||||
},
|
||||
"image": {
|
||||
"hint": "上传 PNG 或 JPG 格式的签名图像",
|
||||
"label": "上传签名图像",
|
||||
"placeholder": "选择图像文件"
|
||||
},
|
||||
"instructions": {
|
||||
"title": "如何添加签名"
|
||||
},
|
||||
"results": {
|
||||
"title": "签名结果"
|
||||
},
|
||||
"steps": {
|
||||
"configure": "配置签名"
|
||||
},
|
||||
"submit": "签署文档",
|
||||
"type": {
|
||||
"canvas": "画布",
|
||||
"draw": "绘制",
|
||||
"image": "图像",
|
||||
"text": "文本",
|
||||
"title": "签名类型"
|
||||
}
|
||||
},
|
||||
"flatten": {
|
||||
"tags": "静态、停用、非交互、简化",
|
||||
@@ -1640,7 +1761,7 @@
|
||||
"header": "展平 PDF",
|
||||
"flattenOnlyForms": "仅展平表格",
|
||||
"submit": "展平",
|
||||
"filenamePrefix": "flattened",
|
||||
"filenamePrefix": "已扁平化",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1651,7 +1772,8 @@
|
||||
"stepTitle": "扁平化选项",
|
||||
"title": "扁平化选项",
|
||||
"flattenOnlyForms.desc": "仅扁平化表单字段,保留其他交互元素",
|
||||
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。"
|
||||
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。",
|
||||
"flattenOnlyForms": "仅扁平化表单"
|
||||
},
|
||||
"results": {
|
||||
"title": "扁平化结果"
|
||||
@@ -1687,7 +1809,7 @@
|
||||
"header": "修复 PDF",
|
||||
"submit": "修复",
|
||||
"description": "该工具将尝试修复损坏或受损的 PDF 文件。无需额外设置。",
|
||||
"filenamePrefix": "repaired",
|
||||
"filenamePrefix": "已修复",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1747,7 +1869,17 @@
|
||||
"tags": "评论、高亮、笔记、标注、删除",
|
||||
"title": "删除标注",
|
||||
"header": "删除标注",
|
||||
"submit": "删除"
|
||||
"submit": "删除",
|
||||
"error": {
|
||||
"failed": "从 PDF 删除注释时发生错误。"
|
||||
},
|
||||
"info": {
|
||||
"description": "此工具将从您的 PDF 文档中删除所有注释(评论、高亮、笔记等)。",
|
||||
"title": "关于删除注释"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置"
|
||||
}
|
||||
},
|
||||
"compare": {
|
||||
"tags": "区分、对比、更改、分析",
|
||||
@@ -1779,7 +1911,7 @@
|
||||
"certSign": {
|
||||
"tags": "身份验证、PEM、P12、官方、加密",
|
||||
"title": "证书签名",
|
||||
"filenamePrefix": "signed",
|
||||
"filenamePrefix": "已签名",
|
||||
"signMode": {
|
||||
"stepTitle": "签名模式",
|
||||
"tooltip": {
|
||||
@@ -1903,7 +2035,7 @@
|
||||
"selectPDF": "选择 PDF 文件:",
|
||||
"submit": "移除签名",
|
||||
"description": "该工具将从您的 PDF 文档中移除数字证书签名。",
|
||||
"filenamePrefix": "unsigned",
|
||||
"filenamePrefix": "未签名",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -1923,7 +2055,7 @@
|
||||
"submit": "提交"
|
||||
},
|
||||
"bookletImposition": {
|
||||
"tags": "booklet,imposition,printing,binding,folding,signature",
|
||||
"tags": "小册子,拼版,打印,装订,折叠,签名",
|
||||
"title": "小册子拼版",
|
||||
"header": "小册子拼版",
|
||||
"submit": "创建小册子",
|
||||
@@ -2024,7 +2156,7 @@
|
||||
"submit": "提交"
|
||||
},
|
||||
"adjustPageScale": {
|
||||
"tags": "resize,modify,dimension,adapt",
|
||||
"tags": "调整大小,修改,尺寸,适应",
|
||||
"title": "调整页面比例",
|
||||
"header": "调整页面比例",
|
||||
"scaleFactor": {
|
||||
@@ -2547,7 +2679,7 @@
|
||||
"header": "将 PDF 转换为单页",
|
||||
"submit": "转为单页",
|
||||
"description": "该工具会将 PDF 的所有页面合并为一张超长单页。宽度保持与原页面相同,高度为所有页面高度之和。",
|
||||
"filenamePrefix": "single_page",
|
||||
"filenamePrefix": "单页",
|
||||
"files": {
|
||||
"placeholder": "在主视图中选择一个 PDF 文件以开始"
|
||||
},
|
||||
@@ -2768,7 +2900,7 @@
|
||||
"title": "API 文档",
|
||||
"header": "API 文档",
|
||||
"desc": "查看并测试 Stirling PDF 的 API 端点",
|
||||
"tags": "api,documentation,swagger,endpoints,development"
|
||||
"tags": "api,文档,swagger,端点,开发"
|
||||
},
|
||||
"cookieBanner": {
|
||||
"popUp": {
|
||||
@@ -3006,7 +3138,7 @@
|
||||
"completed": "安全清理成功完成",
|
||||
"error.generic": "安全清理失败",
|
||||
"error.failed": "安全清理 PDF 时发生错误。",
|
||||
"filenamePrefix": "sanitised",
|
||||
"filenamePrefix": "已清理",
|
||||
"sanitizationResults": "安全清理结果",
|
||||
"steps": {
|
||||
"files": "文件",
|
||||
@@ -3024,7 +3156,13 @@
|
||||
"removeXMPMetadata.desc": "从 PDF 中移除 XMP 元数据",
|
||||
"removeMetadata.desc": "移除文档信息元数据(标题、作者等)",
|
||||
"removeLinks.desc": "移除外部链接与启动动作",
|
||||
"removeFonts.desc": "从 PDF 中移除嵌入字体"
|
||||
"removeFonts.desc": "从 PDF 中移除嵌入字体",
|
||||
"removeEmbeddedFiles": "删除嵌入文件",
|
||||
"removeFonts": "删除字体",
|
||||
"removeJavaScript": "删除 JavaScript",
|
||||
"removeLinks": "删除链接",
|
||||
"removeMetadata": "删除文档元数据",
|
||||
"removeXMPMetadata": "删除 XMP 元数据"
|
||||
}
|
||||
},
|
||||
"addPassword": {
|
||||
@@ -3032,7 +3170,7 @@
|
||||
"desc": "使用密码加密您的 PDF 文档。",
|
||||
"completed": "已应用密码保护",
|
||||
"submit": "加密",
|
||||
"filenamePrefix": "encrypted",
|
||||
"filenamePrefix": "已加密",
|
||||
"error": {
|
||||
"failed": "加密 PDF 时发生错误。"
|
||||
},
|
||||
@@ -3141,7 +3279,7 @@
|
||||
"placeholder": "输入当前密码",
|
||||
"completed": "密码已配置"
|
||||
},
|
||||
"filenamePrefix": "decrypted",
|
||||
"filenamePrefix": "已解密",
|
||||
"error": {
|
||||
"failed": "移除 PDF 密码时发生错误。"
|
||||
},
|
||||
@@ -3294,5 +3432,56 @@
|
||||
}
|
||||
},
|
||||
"termsAndConditions": "条款与条件",
|
||||
"logOut": "退出登录"
|
||||
"logOut": "退出登录",
|
||||
"AddAttachmentsRequest": {
|
||||
"addMoreFiles": "添加更多文件...",
|
||||
"attachments": "选择附件",
|
||||
"info": "选择要附加到 PDF 的文件。这些文件将被嵌入并可通过 PDF 的附件面板访问。",
|
||||
"placeholder": "选择文件...",
|
||||
"results": {
|
||||
"title": "附件结果"
|
||||
},
|
||||
"selectFiles": "选择要附加的文件",
|
||||
"selectedFiles": "已选择的文件",
|
||||
"submit": "添加附件"
|
||||
},
|
||||
"applyAndContinue": "应用并继续",
|
||||
"discardChanges": "放弃更改",
|
||||
"exportAndContinue": "导出并继续",
|
||||
"keepWorking": "继续工作",
|
||||
"replaceColor": {
|
||||
"tags": "替换颜色,页面操作,后端,服务器端"
|
||||
},
|
||||
"scannerImageSplit": {
|
||||
"error": {
|
||||
"failed": "提取图像扫描时发生错误。"
|
||||
},
|
||||
"submit": "提取图像扫描",
|
||||
"title": "已提取的图像",
|
||||
"tooltip": {
|
||||
"headsUp": "注意",
|
||||
"headsUpDesc": "重叠的照片或颜色与照片非常接近的背景会降低准确性 - 尝试使用更浅或更深的背景并留出更多空间。",
|
||||
"problem1": "未检测到照片 → 将容差增加到 30-50",
|
||||
"problem2": "误检测太多 → 将最小面积增加到 15,000-20,000",
|
||||
"problem3": "裁剪太紧 → 将边框大小增加到 5-10",
|
||||
"problem4": "倾斜的照片未矫正 → 将角度阈值降低到 ~5°",
|
||||
"problem5": "灰尘/噪声框 → 将最小轮廓面积增加到 1000-2000",
|
||||
"quickFixes": "快速修复",
|
||||
"setupTips": "设置提示",
|
||||
"tip1": "使用简单的浅色背景",
|
||||
"tip2": "在照片之间留出小间隙(≈1 厘米)",
|
||||
"tip3": "以 300-600 DPI 扫描",
|
||||
"tip4": "清洁扫描仪玻璃",
|
||||
"title": "照片分割器",
|
||||
"useCase1": "一次扫描整个相册页面",
|
||||
"useCase2": "将平板批次拆分为单独的文件",
|
||||
"useCase3": "将拼贴画拆分为单独的照片",
|
||||
"useCase4": "从文档中提取照片",
|
||||
"whatThisDoes": "功能说明",
|
||||
"whatThisDoesDesc": "自动查找并从扫描页面或合成图像中提取每张照片 - 无需手动裁剪。",
|
||||
"whenToUse": "何时使用"
|
||||
}
|
||||
},
|
||||
"unsavedChanges": "您的 PDF 有未保存的更改。您想做什么?",
|
||||
"unsavedChangesTitle": "未保存的更改"
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const { execSync } = require('node:child_process');
|
||||
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
import { argv } from 'node:process';
|
||||
const { argv } = require('node:process');
|
||||
const inputIdx = argv.indexOf('--input');
|
||||
const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null;
|
||||
const POSTPROCESS_ONLY = !!INPUT_FILE;
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
// __dirname is available in CommonJS by default
|
||||
|
||||
/**
|
||||
* Generate 3rd party licenses for frontend dependencies
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
|
||||
import { FileContextProvider } from "./contexts/FileContext";
|
||||
import { NavigationProvider } from "./contexts/NavigationContext";
|
||||
@@ -14,6 +14,7 @@ import "./styles/cookieconsent.css";
|
||||
import "./index.css";
|
||||
import { RightRailProvider } from "./contexts/RightRailContext";
|
||||
import { ViewerProvider } from "./contexts/ViewerContext";
|
||||
import { SignatureProvider } from "./contexts/SignatureContext";
|
||||
|
||||
// Import file ID debugging helpers (development only)
|
||||
import "./utils/fileIdSafety";
|
||||
@@ -45,9 +46,11 @@ export default function App() {
|
||||
<ToolWorkflowProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
<HomePage />
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</ToolWorkflowProvider>
|
||||
|
||||
@@ -7,6 +7,132 @@
|
||||
"moduleLicense": "Apache-2.0",
|
||||
"moduleLicenseUrl": "git+https://github.com/atlassian/pragmatic-drag-and-drop.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/core",
|
||||
"moduleUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/engines",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-annotation",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-export",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-history",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-interaction-manager",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-loader",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-pan",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-render",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-rotate",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-scroll",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-search",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-selection",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-spread",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-thumbnail",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-tiling",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-viewport",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@embedpdf/plugin-zoom",
|
||||
"moduleUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git",
|
||||
"moduleVersion": "1.3.0",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/embedpdf/embed-pdf-viewer.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@emotion/react",
|
||||
"moduleUrl": "git+https://github.com/emotion-js/emotion.git#main",
|
||||
@@ -35,6 +161,13 @@
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dates",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
"moduleVersion": "8.3.1",
|
||||
"moduleLicense": "MIT",
|
||||
"moduleLicenseUrl": "git+https://github.com/mantinedev/mantine.git"
|
||||
},
|
||||
{
|
||||
"moduleName": "@mantine/dropzone",
|
||||
"moduleUrl": "git+https://github.com/mantinedev/mantine.git",
|
||||
@@ -143,7 +276,7 @@
|
||||
{
|
||||
"moduleName": "posthog-js",
|
||||
"moduleUrl": "git+https://github.com/PostHog/posthog-js.git",
|
||||
"moduleVersion": "1.266.0",
|
||||
"moduleVersion": "1.268.0",
|
||||
"moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
|
||||
"moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
|
||||
interface PDFAnnotationContextValue {
|
||||
// Drawing mode management
|
||||
activateDrawMode: () => void;
|
||||
deactivateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
|
||||
// Drawing settings
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
|
||||
// History operations
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
|
||||
// Image data management
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
|
||||
// Placement state
|
||||
isPlacementMode: boolean;
|
||||
|
||||
// Signature configuration
|
||||
signatureConfig: any | null;
|
||||
setSignatureConfig: (config: any | null) => void;
|
||||
}
|
||||
|
||||
const PDFAnnotationContext = createContext<PDFAnnotationContextValue | undefined>(undefined);
|
||||
|
||||
interface PDFAnnotationProviderProps {
|
||||
children: ReactNode;
|
||||
// These would come from the signature context
|
||||
activateDrawMode: () => void;
|
||||
deactivateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
isPlacementMode: boolean;
|
||||
signatureConfig: any | null;
|
||||
setSignatureConfig: (config: any | null) => void;
|
||||
}
|
||||
|
||||
export const PDFAnnotationProvider: React.FC<PDFAnnotationProviderProps> = ({
|
||||
children,
|
||||
activateDrawMode,
|
||||
deactivateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
isPlacementMode,
|
||||
signatureConfig,
|
||||
setSignatureConfig
|
||||
}) => {
|
||||
const contextValue: PDFAnnotationContextValue = {
|
||||
activateDrawMode,
|
||||
deactivateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
isPlacementMode,
|
||||
signatureConfig,
|
||||
setSignatureConfig
|
||||
};
|
||||
|
||||
return (
|
||||
<PDFAnnotationContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</PDFAnnotationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePDFAnnotation = (): PDFAnnotationContextValue => {
|
||||
const context = useContext(PDFAnnotationContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('usePDFAnnotation must be used within a PDFAnnotationProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack, Alert, Text } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DrawingControls } from './DrawingControls';
|
||||
import { ColorPicker } from './ColorPicker';
|
||||
import { usePDFAnnotation } from '../providers/PDFAnnotationProvider';
|
||||
|
||||
export interface AnnotationToolConfig {
|
||||
enableDrawing?: boolean;
|
||||
enableImageUpload?: boolean;
|
||||
enableTextInput?: boolean;
|
||||
showPlaceButton?: boolean;
|
||||
placeButtonText?: string;
|
||||
}
|
||||
|
||||
interface BaseAnnotationToolProps {
|
||||
config: AnnotationToolConfig;
|
||||
children: React.ReactNode;
|
||||
onSignatureDataChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const BaseAnnotationTool: React.FC<BaseAnnotationToolProps> = ({
|
||||
config,
|
||||
children,
|
||||
onSignatureDataChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
activateSignaturePlacementMode,
|
||||
undo,
|
||||
redo
|
||||
} = usePDFAnnotation();
|
||||
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
const [signatureData, setSignatureData] = useState<string | null>(null);
|
||||
|
||||
const handleSignatureDataChange = (data: string | null) => {
|
||||
setSignatureData(data);
|
||||
onSignatureDataChange?.(data);
|
||||
};
|
||||
|
||||
const handlePlaceSignature = () => {
|
||||
if (activateSignaturePlacementMode) {
|
||||
activateSignaturePlacementMode();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Drawing Controls (Undo/Redo/Place) */}
|
||||
<DrawingControls
|
||||
onUndo={undo}
|
||||
onRedo={redo}
|
||||
onPlaceSignature={config.showPlaceButton ? handlePlaceSignature : undefined}
|
||||
hasSignatureData={!!signatureData}
|
||||
disabled={disabled}
|
||||
showPlaceButton={config.showPlaceButton}
|
||||
placeButtonText={config.placeButtonText}
|
||||
/>
|
||||
|
||||
{/* Tool Content */}
|
||||
{React.cloneElement(children as React.ReactElement<any>, {
|
||||
selectedColor,
|
||||
signatureData,
|
||||
onSignatureDataChange: handleSignatureDataChange,
|
||||
onColorSwatchClick: () => setIsColorPickerOpen(true),
|
||||
disabled
|
||||
})}
|
||||
|
||||
{/* Instructions for placing signature */}
|
||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||
<Text size="sm">
|
||||
Click anywhere on the PDF to place your annotation.
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={setSelectedColor}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Modal, Stack, ColorPicker as MantineColorPicker, Group, Button, ColorSwatch } from '@mantine/core';
|
||||
|
||||
interface ColorPickerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
selectedColor: string;
|
||||
onColorChange: (color: string) => void;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export const ColorPicker: React.FC<ColorPickerProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
selectedColor,
|
||||
onColorChange,
|
||||
title = "Choose Color"
|
||||
}) => {
|
||||
return (
|
||||
<Modal
|
||||
opened={isOpen}
|
||||
onClose={onClose}
|
||||
title={title}
|
||||
size="sm"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
<MantineColorPicker
|
||||
format="hex"
|
||||
value={selectedColor}
|
||||
onChange={onColorChange}
|
||||
swatches={['#000000', '#0066cc', '#cc0000', '#cc6600', '#009900', '#6600cc']}
|
||||
swatchesPerRow={6}
|
||||
size="lg"
|
||||
fullWidth
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button onClick={onClose}>
|
||||
Done
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
interface ColorSwatchButtonProps {
|
||||
color: string;
|
||||
onClick: () => void;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const ColorSwatchButton: React.FC<ColorSwatchButtonProps> = ({
|
||||
color,
|
||||
onClick,
|
||||
size = 24
|
||||
}) => {
|
||||
return (
|
||||
<ColorSwatch
|
||||
color={color}
|
||||
size={size}
|
||||
radius={0}
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,437 @@
|
||||
import React, { useRef, useState, useCallback } from 'react';
|
||||
import { Paper, Group, Button, Modal, Stack, Text } from '@mantine/core';
|
||||
import { ColorSwatchButton } from './ColorPicker';
|
||||
import PenSizeSelector from '../../tools/sign/PenSizeSelector';
|
||||
|
||||
interface DrawingCanvasProps {
|
||||
selectedColor: string;
|
||||
penSize: number;
|
||||
penSizeInput: string;
|
||||
onColorSwatchClick: () => void;
|
||||
onPenSizeChange: (size: number) => void;
|
||||
onPenSizeInputChange: (input: string) => void;
|
||||
onSignatureDataChange: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
modalWidth?: number;
|
||||
modalHeight?: number;
|
||||
additionalButtons?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
||||
selectedColor,
|
||||
penSize,
|
||||
penSizeInput,
|
||||
onColorSwatchClick,
|
||||
onPenSizeChange,
|
||||
onPenSizeInputChange,
|
||||
onSignatureDataChange,
|
||||
disabled = false,
|
||||
width = 400,
|
||||
height = 150,
|
||||
modalWidth = 800,
|
||||
modalHeight = 400,
|
||||
additionalButtons
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const modalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const visibleModalCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const [isDrawing, setIsDrawing] = useState(false);
|
||||
const [isModalDrawing, setIsModalDrawing] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// Drawing functions for main canvas
|
||||
const startDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!canvasRef.current || disabled) return;
|
||||
|
||||
setIsDrawing(true);
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const scaleX = canvasRef.current.width / rect.width;
|
||||
const scaleY = canvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
}
|
||||
}, [disabled, selectedColor, penSize]);
|
||||
|
||||
const draw = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!isDrawing || !canvasRef.current || disabled) return;
|
||||
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const scaleX = canvasRef.current.width / rect.width;
|
||||
const scaleY = canvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}, [isDrawing, disabled]);
|
||||
|
||||
const stopDrawing = useCallback(() => {
|
||||
if (!isDrawing || disabled) return;
|
||||
|
||||
setIsDrawing(false);
|
||||
|
||||
// Save canvas as signature data
|
||||
if (canvasRef.current) {
|
||||
const dataURL = canvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
}
|
||||
}, [isDrawing, disabled, onSignatureDataChange]);
|
||||
|
||||
// Modal canvas drawing functions
|
||||
const startModalDrawing = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||
|
||||
setIsModalDrawing(true);
|
||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Draw on both the visible modal canvas and hidden canvas
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
|
||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
}
|
||||
});
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
const drawModal = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (!isModalDrawing || !visibleModalCanvasRef.current || !modalCanvasRef.current) return;
|
||||
|
||||
const rect = visibleModalCanvasRef.current.getBoundingClientRect();
|
||||
const scaleX = visibleModalCanvasRef.current.width / rect.width;
|
||||
const scaleY = visibleModalCanvasRef.current.height / rect.height;
|
||||
const x = (e.clientX - rect.left) * scaleX;
|
||||
const y = (e.clientY - rect.top) * scaleY;
|
||||
|
||||
// Draw on both canvases
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
|
||||
[visibleCtx, hiddenCtx].forEach(ctx => {
|
||||
if (ctx) {
|
||||
ctx.lineTo(x, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
}, [isModalDrawing]);
|
||||
|
||||
const stopModalDrawing = useCallback(() => {
|
||||
if (!isModalDrawing) return;
|
||||
setIsModalDrawing(false);
|
||||
|
||||
// Sync the canvases and update signature data (only when drawing stops)
|
||||
if (modalCanvasRef.current) {
|
||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
|
||||
// Also update the small canvas display
|
||||
if (canvasRef.current) {
|
||||
const smallCtx = canvasRef.current.getContext('2d');
|
||||
if (smallCtx) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
smallCtx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
smallCtx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
};
|
||||
img.src = dataURL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isModalDrawing]);
|
||||
|
||||
// Clear canvas functions
|
||||
const clearCanvas = useCallback(() => {
|
||||
if (!canvasRef.current || disabled) return;
|
||||
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
|
||||
// Also clear the modal canvas if it exists
|
||||
if (modalCanvasRef.current) {
|
||||
const modalCtx = modalCanvasRef.current.getContext('2d');
|
||||
if (modalCtx) {
|
||||
modalCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
onSignatureDataChange(null);
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const clearModalCanvas = useCallback(() => {
|
||||
// Clear both modal canvases (visible and hidden)
|
||||
if (modalCanvasRef.current) {
|
||||
const hiddenCtx = modalCanvasRef.current.getContext('2d');
|
||||
if (hiddenCtx) {
|
||||
hiddenCtx.clearRect(0, 0, modalCanvasRef.current.width, modalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
if (visibleModalCanvasRef.current) {
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
if (visibleCtx) {
|
||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Also clear the main canvas and signature data
|
||||
if (canvasRef.current) {
|
||||
const mainCtx = canvasRef.current.getContext('2d');
|
||||
if (mainCtx) {
|
||||
mainCtx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
|
||||
}
|
||||
}
|
||||
|
||||
onSignatureDataChange(null);
|
||||
}, []);
|
||||
|
||||
const saveModalSignature = useCallback(() => {
|
||||
if (!modalCanvasRef.current) return;
|
||||
|
||||
const dataURL = modalCanvasRef.current.toDataURL('image/png');
|
||||
onSignatureDataChange(dataURL);
|
||||
|
||||
// Copy to small canvas for display
|
||||
if (canvasRef.current) {
|
||||
const ctx = canvasRef.current.getContext('2d');
|
||||
if (ctx) {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
ctx.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
ctx.drawImage(img, 0, 0, canvasRef.current!.width, canvasRef.current!.height);
|
||||
};
|
||||
img.src = dataURL;
|
||||
}
|
||||
}
|
||||
|
||||
setIsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const openModal = useCallback(() => {
|
||||
setIsModalOpen(true);
|
||||
// Copy content to modal canvas after a brief delay
|
||||
setTimeout(() => {
|
||||
if (visibleModalCanvasRef.current && modalCanvasRef.current) {
|
||||
const visibleCtx = visibleModalCanvasRef.current.getContext('2d');
|
||||
if (visibleCtx) {
|
||||
visibleCtx.strokeStyle = selectedColor;
|
||||
visibleCtx.lineWidth = penSize;
|
||||
visibleCtx.lineCap = 'round';
|
||||
visibleCtx.lineJoin = 'round';
|
||||
visibleCtx.clearRect(0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
visibleCtx.drawImage(modalCanvasRef.current, 0, 0, visibleModalCanvasRef.current.width, visibleModalCanvasRef.current.height);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
// Initialize canvas settings whenever color or pen size changes
|
||||
React.useEffect(() => {
|
||||
const updateCanvas = (canvas: HTMLCanvasElement | null) => {
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.strokeStyle = selectedColor;
|
||||
ctx.lineWidth = penSize;
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
}
|
||||
};
|
||||
|
||||
updateCanvas(canvasRef.current);
|
||||
updateCanvas(modalCanvasRef.current);
|
||||
updateCanvas(visibleModalCanvasRef.current);
|
||||
}, [selectedColor, penSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Paper withBorder p="md">
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between">
|
||||
<Text fw={500}>Draw your signature</Text>
|
||||
<Group gap="lg">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs" ta="center">Color</Text>
|
||||
<Group justify="center">
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
onClick={onColorSwatchClick}
|
||||
/>
|
||||
</Group>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||
<PenSizeSelector
|
||||
value={penSize}
|
||||
inputValue={penSizeInput}
|
||||
onValueChange={onPenSizeChange}
|
||||
onInputChange={onPenSizeInputChange}
|
||||
disabled={disabled}
|
||||
placeholder="Size"
|
||||
size="compact-sm"
|
||||
style={{ width: '60px' }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ paddingTop: '24px' }}>
|
||||
<Button
|
||||
variant="light"
|
||||
size="compact-sm"
|
||||
onClick={openModal}
|
||||
disabled={disabled}
|
||||
>
|
||||
Expand
|
||||
</Button>
|
||||
</div>
|
||||
</Group>
|
||||
</Group>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: disabled ? 'default' : 'crosshair',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
}}
|
||||
onMouseDown={startDrawing}
|
||||
onMouseMove={draw}
|
||||
onMouseUp={stopDrawing}
|
||||
onMouseLeave={stopDrawing}
|
||||
/>
|
||||
<Group justify="space-between">
|
||||
<div>
|
||||
{additionalButtons}
|
||||
</div>
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
size="compact-sm"
|
||||
onClick={clearCanvas}
|
||||
disabled={disabled}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
{/* Hidden canvas for modal synchronization */}
|
||||
<canvas
|
||||
ref={modalCanvasRef}
|
||||
width={modalWidth}
|
||||
height={modalHeight}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
|
||||
{/* Modal for larger signature canvas */}
|
||||
<Modal
|
||||
opened={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
title="Draw Your Signature"
|
||||
size="xl"
|
||||
centered
|
||||
>
|
||||
<Stack gap="md">
|
||||
{/* Color and Pen Size picker */}
|
||||
<Paper withBorder p="sm">
|
||||
<Group gap="lg" align="flex-end">
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Color</Text>
|
||||
<ColorSwatchButton
|
||||
color={selectedColor}
|
||||
onClick={onColorSwatchClick}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb="xs">Pen Size</Text>
|
||||
<PenSizeSelector
|
||||
value={penSize}
|
||||
inputValue={penSizeInput}
|
||||
onValueChange={onPenSizeChange}
|
||||
onInputChange={onPenSizeInputChange}
|
||||
placeholder="Size"
|
||||
size="compact-sm"
|
||||
style={{ width: '60px' }}
|
||||
/>
|
||||
</div>
|
||||
</Group>
|
||||
</Paper>
|
||||
|
||||
<Paper withBorder p="md">
|
||||
<canvas
|
||||
ref={visibleModalCanvasRef}
|
||||
width={modalWidth}
|
||||
height={modalHeight}
|
||||
style={{
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
cursor: 'crosshair',
|
||||
backgroundColor: '#ffffff',
|
||||
width: '100%',
|
||||
maxWidth: `${modalWidth}px`,
|
||||
height: 'auto',
|
||||
}}
|
||||
onMouseDown={startModalDrawing}
|
||||
onMouseMove={drawModal}
|
||||
onMouseUp={stopModalDrawing}
|
||||
onMouseLeave={stopModalDrawing}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Group justify="space-between">
|
||||
<Button
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={clearModalCanvas}
|
||||
>
|
||||
Clear Canvas
|
||||
</Button>
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="subtle"
|
||||
onClick={() => setIsModalOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={saveModalSignature}
|
||||
>
|
||||
Save Signature
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DrawingCanvas;
|
||||
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Group, Button } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DrawingControlsProps {
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
onPlaceSignature?: () => void;
|
||||
hasSignatureData?: boolean;
|
||||
disabled?: boolean;
|
||||
showPlaceButton?: boolean;
|
||||
placeButtonText?: string;
|
||||
}
|
||||
|
||||
export const DrawingControls: React.FC<DrawingControlsProps> = ({
|
||||
onUndo,
|
||||
onRedo,
|
||||
onPlaceSignature,
|
||||
hasSignatureData = false,
|
||||
disabled = false,
|
||||
showPlaceButton = true,
|
||||
placeButtonText = "Update and Place"
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Group gap="sm">
|
||||
{/* Undo/Redo Controls */}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onUndo}
|
||||
disabled={disabled}
|
||||
flex={1}
|
||||
>
|
||||
{t('sign.undo', 'Undo')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRedo}
|
||||
disabled={disabled}
|
||||
flex={1}
|
||||
>
|
||||
{t('sign.redo', 'Redo')}
|
||||
</Button>
|
||||
|
||||
{/* Place Signature Button */}
|
||||
{showPlaceButton && onPlaceSignature && (
|
||||
<Button
|
||||
variant="filled"
|
||||
color="blue"
|
||||
onClick={onPlaceSignature}
|
||||
disabled={disabled || !hasSignatureData}
|
||||
flex={1}
|
||||
>
|
||||
{placeButtonText}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { FileInput, Text, Stack } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface ImageUploaderProps {
|
||||
onImageChange: (file: File | null) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||
onImageChange,
|
||||
disabled = false,
|
||||
label,
|
||||
placeholder,
|
||||
hint
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleImageChange = async (file: File | null) => {
|
||||
if (file && !disabled) {
|
||||
try {
|
||||
// Validate that it's actually an image file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
console.error('Selected file is not an image');
|
||||
return;
|
||||
}
|
||||
|
||||
onImageChange(file);
|
||||
} catch (error) {
|
||||
console.error('Error processing image file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
// Clear image data when no file is selected
|
||||
onImageChange(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<FileInput
|
||||
label={label || t('sign.image.label', 'Upload signature image')}
|
||||
placeholder={placeholder || t('sign.image.placeholder', 'Select image file')}
|
||||
accept="image/*"
|
||||
onChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Text size="sm" c="dimmed">
|
||||
{hint || t('sign.image.hint', 'Upload a PNG or JPG image of your signature')}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Stack, TextInput, Select, Combobox, useCombobox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface TextInputWithFontProps {
|
||||
text: string;
|
||||
onTextChange: (text: string) => void;
|
||||
fontSize: number;
|
||||
onFontSizeChange: (size: number) => void;
|
||||
fontFamily: string;
|
||||
onFontFamilyChange: (family: string) => void;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export const TextInputWithFont: React.FC<TextInputWithFontProps> = ({
|
||||
text,
|
||||
onTextChange,
|
||||
fontSize,
|
||||
onFontSizeChange,
|
||||
fontFamily,
|
||||
onFontFamilyChange,
|
||||
disabled = false,
|
||||
label,
|
||||
placeholder
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [fontSizeInput, setFontSizeInput] = useState(fontSize.toString());
|
||||
const fontSizeCombobox = useCombobox();
|
||||
|
||||
// Sync font size input with prop changes
|
||||
useEffect(() => {
|
||||
setFontSizeInput(fontSize.toString());
|
||||
}, [fontSize]);
|
||||
|
||||
const fontOptions = [
|
||||
{ value: 'Helvetica', label: 'Helvetica' },
|
||||
{ value: 'Times-Roman', label: 'Times' },
|
||||
{ value: 'Courier', label: 'Courier' },
|
||||
{ value: 'Arial', label: 'Arial' },
|
||||
{ value: 'Georgia', label: 'Georgia' },
|
||||
];
|
||||
|
||||
const fontSizeOptions = ['8', '12', '16', '20', '24', '28', '32', '36', '40', '48'];
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<TextInput
|
||||
label={label || t('sign.text.name', 'Signer Name')}
|
||||
placeholder={placeholder || t('sign.text.placeholder', 'Enter your full name')}
|
||||
value={text}
|
||||
onChange={(e) => onTextChange(e.target.value)}
|
||||
disabled={disabled}
|
||||
required
|
||||
/>
|
||||
|
||||
{/* Font Selection */}
|
||||
<Select
|
||||
label="Font"
|
||||
value={fontFamily}
|
||||
onChange={(value) => onFontFamilyChange(value || 'Helvetica')}
|
||||
data={fontOptions}
|
||||
disabled={disabled}
|
||||
searchable
|
||||
allowDeselect={false}
|
||||
/>
|
||||
|
||||
{/* Font Size */}
|
||||
<Combobox
|
||||
onOptionSubmit={(optionValue) => {
|
||||
setFontSizeInput(optionValue);
|
||||
const size = parseInt(optionValue);
|
||||
if (!isNaN(size)) {
|
||||
onFontSizeChange(size);
|
||||
}
|
||||
fontSizeCombobox.closeDropdown();
|
||||
}}
|
||||
store={fontSizeCombobox}
|
||||
withinPortal={false}
|
||||
>
|
||||
<Combobox.Target>
|
||||
<TextInput
|
||||
label="Font Size"
|
||||
placeholder="Type or select font size (8-72)"
|
||||
value={fontSizeInput}
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
setFontSizeInput(value);
|
||||
|
||||
// Parse and validate the typed value in real-time
|
||||
const size = parseInt(value);
|
||||
if (!isNaN(size) && size >= 8 && size <= 72) {
|
||||
onFontSizeChange(size);
|
||||
}
|
||||
|
||||
fontSizeCombobox.openDropdown();
|
||||
fontSizeCombobox.updateSelectedOptionIndex();
|
||||
}}
|
||||
onClick={() => fontSizeCombobox.openDropdown()}
|
||||
onFocus={() => fontSizeCombobox.openDropdown()}
|
||||
onBlur={() => {
|
||||
fontSizeCombobox.closeDropdown();
|
||||
// Clean up invalid values on blur
|
||||
const size = parseInt(fontSizeInput);
|
||||
if (isNaN(size) || size < 8 || size > 72) {
|
||||
setFontSizeInput(fontSize.toString());
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Combobox.Target>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>
|
||||
{fontSizeOptions.map((size) => (
|
||||
<Combobox.Option value={size} key={size}>
|
||||
{size}px
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { DrawingCanvas } from '../shared/DrawingCanvas';
|
||||
|
||||
interface DrawingToolProps {
|
||||
onDrawingChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const DrawingTool: React.FC<DrawingToolProps> = ({
|
||||
onDrawingChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [selectedColor] = useState('#000000');
|
||||
const [penSize, setPenSize] = useState(2);
|
||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||
|
||||
const toolConfig = {
|
||||
enableDrawing: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Drawing"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={onDrawingChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<DrawingCanvas
|
||||
selectedColor={selectedColor}
|
||||
penSize={penSize}
|
||||
penSizeInput={penSizeInput}
|
||||
onColorSwatchClick={() => {}} // Color picker handled by BaseAnnotationTool
|
||||
onPenSizeChange={setPenSize}
|
||||
onPenSizeInputChange={setPenSizeInput}
|
||||
onSignatureDataChange={onDrawingChange || (() => {})}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { ImageUploader } from '../shared/ImageUploader';
|
||||
|
||||
interface ImageToolProps {
|
||||
onImageChange?: (data: string | null) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const ImageTool: React.FC<ImageToolProps> = ({
|
||||
onImageChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [, setImageData] = useState<string | null>(null);
|
||||
|
||||
const handleImageUpload = async (file: File | null) => {
|
||||
if (file && !disabled) {
|
||||
try {
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
resolve(e.target.result as string);
|
||||
} else {
|
||||
reject(new Error('Failed to read file'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
setImageData(result);
|
||||
onImageChange?.(result);
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
setImageData(null);
|
||||
onImageChange?.(null);
|
||||
}
|
||||
};
|
||||
|
||||
const toolConfig = {
|
||||
enableImageUpload: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Image"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={onImageChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<ImageUploader
|
||||
onImageChange={handleImageUpload}
|
||||
disabled={disabled}
|
||||
label="Upload Image"
|
||||
placeholder="Select image file"
|
||||
hint="Upload a PNG, JPG, or other image file to place on the PDF"
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import { BaseAnnotationTool } from '../shared/BaseAnnotationTool';
|
||||
import { TextInputWithFont } from '../shared/TextInputWithFont';
|
||||
|
||||
interface TextToolProps {
|
||||
onTextChange?: (text: string) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TextTool: React.FC<TextToolProps> = ({
|
||||
onTextChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
const [text, setText] = useState('');
|
||||
const [fontSize, setFontSize] = useState(16);
|
||||
const [fontFamily, setFontFamily] = useState('Helvetica');
|
||||
|
||||
const handleTextChange = (newText: string) => {
|
||||
setText(newText);
|
||||
onTextChange?.(newText);
|
||||
};
|
||||
|
||||
const handleSignatureDataChange = (data: string | null) => {
|
||||
if (data) {
|
||||
onTextChange?.(data);
|
||||
}
|
||||
};
|
||||
|
||||
const toolConfig = {
|
||||
enableTextInput: true,
|
||||
showPlaceButton: true,
|
||||
placeButtonText: "Place Text"
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseAnnotationTool
|
||||
config={toolConfig}
|
||||
onSignatureDataChange={handleSignatureDataChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Stack gap="sm">
|
||||
<TextInputWithFont
|
||||
text={text}
|
||||
onTextChange={handleTextChange}
|
||||
fontSize={fontSize}
|
||||
onFontSizeChange={setFontSize}
|
||||
fontFamily={fontFamily}
|
||||
onFontFamilyChange={setFontFamily}
|
||||
disabled={disabled}
|
||||
label="Text Content"
|
||||
placeholder="Enter text to place on the PDF"
|
||||
/>
|
||||
</Stack>
|
||||
</BaseAnnotationTool>
|
||||
);
|
||||
};
|
||||
@@ -149,7 +149,6 @@ export default function Workbench() {
|
||||
<TopControls
|
||||
currentView={currentView}
|
||||
setCurrentView={setCurrentView}
|
||||
selectedToolKey={selectedToolId}
|
||||
/>
|
||||
|
||||
{/* Dismiss All Errors Button */}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useFileState, useFileActions } from "../../contexts/FileContext";
|
||||
import { useNavigationGuard } from "../../contexts/NavigationContext";
|
||||
import { PDFDocument, PageEditorFunctions } from "../../types/pageEditor";
|
||||
import { pdfExportService } from "../../services/pdfExportService";
|
||||
import { documentManipulationService } from "../../services/documentManipulationService";
|
||||
@@ -36,6 +37,9 @@ const PageEditor = ({
|
||||
const { state, selectors } = useFileState();
|
||||
const { actions } = useFileActions();
|
||||
|
||||
// Navigation guard for unsaved changes
|
||||
const { setHasUnsavedChanges } = useNavigationGuard();
|
||||
|
||||
// Prefer IDs + selectors to avoid array identity churn
|
||||
const activeFileIds = state.files.ids;
|
||||
|
||||
@@ -82,6 +86,12 @@ const PageEditor = ({
|
||||
updateUndoRedoState();
|
||||
}, [updateUndoRedoState]);
|
||||
|
||||
// Wrapper for executeCommand to track unsaved changes
|
||||
const executeCommandWithTracking = useCallback((command: any) => {
|
||||
undoManagerRef.current.executeCommand(command);
|
||||
setHasUnsavedChanges(true);
|
||||
}, [setHasUnsavedChanges]);
|
||||
|
||||
// Watch for container size changes to update split line positions
|
||||
useEffect(() => {
|
||||
const container = gridContainerRef.current;
|
||||
@@ -138,17 +148,16 @@ const PageEditor = ({
|
||||
// DOM-first command handlers
|
||||
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
|
||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||
}, []);
|
||||
executeCommandWithTracking(bulkRotateCommand);
|
||||
}, [executeCommandWithTracking]);
|
||||
|
||||
// Command factory functions for PageThumbnail
|
||||
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
|
||||
execute: () => {
|
||||
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
|
||||
|
||||
undoManagerRef.current.executeCommand(bulkRotateCommand);
|
||||
executeCommandWithTracking(bulkRotateCommand);
|
||||
}
|
||||
}), []);
|
||||
}), [executeCommandWithTracking]);
|
||||
|
||||
const createDeleteCommand = useCallback((pageIds: string[]) => ({
|
||||
execute: () => {
|
||||
@@ -174,10 +183,10 @@ const PageEditor = ({
|
||||
() => getPageNumbersFromIds(selectedPageIds),
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}
|
||||
}
|
||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const createSplitCommand = useCallback((position: number) => ({
|
||||
execute: () => {
|
||||
@@ -186,9 +195,9 @@ const PageEditor = ({
|
||||
() => splitPositions,
|
||||
setSplitPositions
|
||||
);
|
||||
undoManagerRef.current.executeCommand(splitCommand);
|
||||
executeCommandWithTracking(splitCommand);
|
||||
}
|
||||
}), [splitPositions]);
|
||||
}), [splitPositions, executeCommandWithTracking]);
|
||||
|
||||
// Command executor for PageThumbnail
|
||||
const executeCommand = useCallback((command: any) => {
|
||||
@@ -232,8 +241,8 @@ const PageEditor = ({
|
||||
() => selectedPageNumbers,
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]);
|
||||
|
||||
const handleDeletePage = useCallback((pageNumber: number) => {
|
||||
if (!displayDocument) return;
|
||||
@@ -251,8 +260,8 @@ const PageEditor = ({
|
||||
() => getPageNumbersFromIds(selectedPageIds),
|
||||
closePdf
|
||||
);
|
||||
undoManagerRef.current.executeCommand(deleteCommand);
|
||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(deleteCommand);
|
||||
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleSplit = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -298,8 +307,8 @@ const PageEditor = ({
|
||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleSplitAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -344,8 +353,8 @@ const PageEditor = ({
|
||||
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
|
||||
};
|
||||
|
||||
undoManagerRef.current.executeCommand(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(smartSplitCommand);
|
||||
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handlePageBreak = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -358,8 +367,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handlePageBreakAll = useCallback(() => {
|
||||
if (!displayDocument || selectedPageIds.length === 0) return;
|
||||
@@ -372,8 +381,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(pageBreakCommand);
|
||||
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
|
||||
if (!displayDocument || files.length === 0) return;
|
||||
@@ -416,8 +425,8 @@ const PageEditor = ({
|
||||
() => displayDocument,
|
||||
setEditedDocument
|
||||
);
|
||||
undoManagerRef.current.executeCommand(reorderCommand);
|
||||
}, [displayDocument, getPageNumbersFromIds]);
|
||||
executeCommandWithTracking(reorderCommand);
|
||||
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
|
||||
|
||||
// Helper function to collect source files for multi-file export
|
||||
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
|
||||
@@ -499,13 +508,14 @@ const PageEditor = ({
|
||||
|
||||
// Step 4: Download the result
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||
|
||||
setExportLoading(false);
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||
|
||||
const onExportAll = useCallback(async () => {
|
||||
if (!displayDocument) return;
|
||||
@@ -552,6 +562,7 @@ const PageEditor = ({
|
||||
const zipFilename = baseExportFilename.replace(/\.pdf$/i, '.zip');
|
||||
|
||||
pdfExportService.downloadFile(zipBlob, zipFilename);
|
||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||
} else {
|
||||
// Single document - regular export
|
||||
const sourceFiles = getSourceFiles();
|
||||
@@ -570,6 +581,7 @@ const PageEditor = ({
|
||||
);
|
||||
|
||||
pdfExportService.downloadFile(result.blob, result.filename);
|
||||
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
|
||||
}
|
||||
|
||||
setExportLoading(false);
|
||||
@@ -577,7 +589,7 @@ const PageEditor = ({
|
||||
console.error('Export failed:', error);
|
||||
setExportLoading(false);
|
||||
}
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
|
||||
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
|
||||
|
||||
// Apply DOM changes to document state using dedicated service
|
||||
const applyChanges = useCallback(() => {
|
||||
@@ -779,7 +791,14 @@ const PageEditor = ({
|
||||
)}
|
||||
|
||||
|
||||
<NavigationWarningModal />
|
||||
<NavigationWarningModal
|
||||
onApplyAndContinue={async () => {
|
||||
applyChanges();
|
||||
}}
|
||||
onExportAndContinue={async () => {
|
||||
await onExportAll();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
Text,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useRef } from "react";
|
||||
import { FileButton, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Flex } from '@mantine/core';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCookieConsent } from '../../hooks/useCookieConsent';
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Box, Group, Text, Button } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -37,7 +36,7 @@ const MultiSelectControls = ({
|
||||
>
|
||||
{t("fileManager.clearSelection", "Clear Selection")}
|
||||
</Button>
|
||||
|
||||
|
||||
{onAddToUpload && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -47,7 +46,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.addToUpload", "Add to Upload")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onOpenInFileEditor && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -58,7 +57,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.openInFileEditor", "Open in File Editor")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onOpenInPageEditor && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -69,7 +68,7 @@ const MultiSelectControls = ({
|
||||
{t("fileManager.openInPageEditor", "Open in Page Editor")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
|
||||
{onDeleteAll && (
|
||||
<Button
|
||||
size="xs"
|
||||
@@ -85,4 +84,4 @@ const MultiSelectControls = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelectControls;
|
||||
export default MultiSelectControls;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Modal, Text, Button, Group, Stack } from '@mantine/core';
|
||||
import { useNavigationGuard } from '../../contexts/NavigationContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface NavigationWarningModalProps {
|
||||
onApplyAndContinue?: () => Promise<void>;
|
||||
@@ -11,6 +11,8 @@ const NavigationWarningModal = ({
|
||||
onApplyAndContinue,
|
||||
onExportAndContinue
|
||||
}: NavigationWarningModalProps) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
showNavigationWarning,
|
||||
hasUnsavedChanges,
|
||||
@@ -28,7 +30,7 @@ const NavigationWarningModal = ({
|
||||
confirmNavigation();
|
||||
};
|
||||
|
||||
const handleApplyAndContinue = async () => {
|
||||
const _handleApplyAndContinue = async () => {
|
||||
if (onApplyAndContinue) {
|
||||
await onApplyAndContinue();
|
||||
}
|
||||
@@ -52,55 +54,59 @@ const NavigationWarningModal = ({
|
||||
<Modal
|
||||
opened={showNavigationWarning}
|
||||
onClose={handleKeepWorking}
|
||||
title="Unsaved Changes"
|
||||
title={t("unsavedChangesTitle", "Unsaved Changes")}
|
||||
centered
|
||||
size="lg"
|
||||
closeOnClickOutside={false}
|
||||
closeOnEscape={false}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text>
|
||||
You have unsaved changes to your PDF. What would you like to do?
|
||||
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
|
||||
</Text>
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="gray"
|
||||
onClick={handleKeepWorking}
|
||||
>
|
||||
Keep Working
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
<Group justify="space-between" gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="red"
|
||||
onClick={handleDiscardChanges}
|
||||
>
|
||||
Discard Changes
|
||||
{t("discardChanges", "Discard Changes")}
|
||||
</Button>
|
||||
|
||||
{onApplyAndContinue && (
|
||||
|
||||
<Group gap="sm">
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={handleApplyAndContinue}
|
||||
color="var(--mantine-color-gray-8)"
|
||||
onClick={handleKeepWorking}
|
||||
>
|
||||
Apply & Continue
|
||||
{t("keepWorking", "Keep Working")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{onExportAndContinue && (
|
||||
<Button
|
||||
color="green"
|
||||
onClick={handleExportAndContinue}
|
||||
>
|
||||
Export & Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* TODO:: Add this back in when it works */}
|
||||
{/* {onApplyAndContinue && (
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
onClick={handleApplyAndContinue}
|
||||
>
|
||||
{t("applyAndContinue", "Apply & Continue")}
|
||||
</Button>
|
||||
)} */}
|
||||
|
||||
{onExportAndContinue && (
|
||||
<Button
|
||||
onClick={handleExportAndContinue}
|
||||
>
|
||||
{t("exportAndContinue", "Export & Continue")}
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavigationWarningModal;
|
||||
export default NavigationWarningModal;
|
||||
|
||||
@@ -41,10 +41,10 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
// Helper function to render navigation buttons with URL support
|
||||
const renderNavButton = (config: ButtonConfig, index: number) => {
|
||||
const isActive = isNavButtonActive(config, activeButton, isFilesModalOpen, configModalOpen, selectedToolKey, leftPanelView);
|
||||
|
||||
|
||||
// Check if this button has URL navigation support
|
||||
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
||||
? getToolNavigation(config.id)
|
||||
const navProps = config.type === 'navigation' && (config.id === 'read' || config.id === 'automate')
|
||||
? getToolNavigation(config.id)
|
||||
: null;
|
||||
|
||||
const handleClick = (e?: React.MouseEvent) => {
|
||||
@@ -59,7 +59,7 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
return (
|
||||
<div key={config.id} className="flex flex-col items-center gap-1" style={{ marginTop: index === 0 ? '0.5rem' : "0rem" }}>
|
||||
<ActionIcon
|
||||
{...(navProps ? {
|
||||
{...(navProps ? {
|
||||
component: "a" as const,
|
||||
href: navProps.href,
|
||||
onClick: (e: React.MouseEvent) => handleClick(e),
|
||||
@@ -249,4 +249,6 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
QuickAccessBar.displayName = 'QuickAccessBar';
|
||||
|
||||
export default QuickAccessBar;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, ReactNode } from 'react';
|
||||
import { createContext, useContext, ReactNode } from 'react';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import { useRainbowTheme } from '../../hooks/useRainbowTheme';
|
||||
import { mantineTheme } from '../../theme/mantineTheme';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Tooltip } from '../shared/Tooltip';
|
||||
import BulkSelectionPanel from '../pageEditor/BulkSelectionPanel';
|
||||
import { SearchInterface } from '../viewer/SearchInterface';
|
||||
import { ViewerContext } from '../../contexts/ViewerContext';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
|
||||
import { parseSelection } from '../../utils/bulkselection/parseSelection';
|
||||
|
||||
@@ -43,6 +44,9 @@ export default function RightRail() {
|
||||
const { selectedFiles, selectedFileIds, setSelectedFiles } = useFileSelection();
|
||||
const { removeFiles } = useFileManagement();
|
||||
|
||||
// Signature context for checking if signatures have been applied
|
||||
const { signaturesApplied } = useSignature();
|
||||
|
||||
const activeFiles = selectors.getFiles();
|
||||
const filesSignature = selectors.getFilesSignature();
|
||||
|
||||
@@ -98,8 +102,14 @@ export default function RightRail() {
|
||||
}
|
||||
}, [currentView, setSelectedFiles, pageEditorFunctions]);
|
||||
|
||||
const handleExportAll = useCallback(() => {
|
||||
const handleExportAll = useCallback(async () => {
|
||||
if (currentView === 'viewer') {
|
||||
// Check if signatures have been applied
|
||||
if (!signaturesApplied) {
|
||||
alert('You have unapplied signatures. Please use "Apply Signatures" first before exporting.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use EmbedPDF export functionality for viewer mode
|
||||
viewerContext?.exportActions?.download();
|
||||
} else if (currentView === 'fileEditor') {
|
||||
@@ -119,7 +129,7 @@ export default function RightRail() {
|
||||
// Export all pages (not just selected)
|
||||
pageEditorFunctions?.onExportAll?.();
|
||||
}
|
||||
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext]);
|
||||
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext, signaturesApplied, selectors, fileActions]);
|
||||
|
||||
const handleCloseSelected = useCallback(() => {
|
||||
if (currentView !== 'fileEditor') return;
|
||||
|
||||
@@ -107,3 +107,5 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
TextInput.displayName = 'TextInput';
|
||||
|
||||
@@ -32,7 +32,7 @@ const ToolChain: React.FC<ToolChainProps> = ({
|
||||
|
||||
const getToolName = (toolId: ToolId) => {
|
||||
return t(`home.${toolId}.title`, toolId);
|
||||
}
|
||||
};
|
||||
|
||||
// Create full tool chain for tooltip
|
||||
const fullChainDisplay = displayStyle === 'badges' ? (
|
||||
|
||||
@@ -15,11 +15,11 @@ const viewOptionStyle = {
|
||||
gap: 6,
|
||||
whiteSpace: 'nowrap',
|
||||
paddingTop: '0.3rem',
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Build view options showing text always
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
|
||||
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => {
|
||||
const viewerOption = {
|
||||
label: (
|
||||
<div style={viewOptionStyle as React.CSSProperties}>
|
||||
@@ -75,7 +75,7 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
// Build options array conditionally
|
||||
return [
|
||||
viewerOption,
|
||||
...(isToolSelected ? [] : [pageEditorOption]),
|
||||
pageEditorOption,
|
||||
fileEditorOption,
|
||||
];
|
||||
};
|
||||
@@ -83,19 +83,15 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
|
||||
interface TopControlsProps {
|
||||
currentView: WorkbenchType;
|
||||
setCurrentView: (view: WorkbenchType) => void;
|
||||
selectedToolKey?: string | null;
|
||||
}
|
||||
|
||||
const TopControls = ({
|
||||
currentView,
|
||||
setCurrentView,
|
||||
selectedToolKey,
|
||||
}: TopControlsProps) => {
|
||||
}: TopControlsProps) => {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
|
||||
|
||||
const isToolSelected = selectedToolKey !== null;
|
||||
|
||||
const handleViewChange = useCallback((view: string) => {
|
||||
if (!isValidWorkbench(view)) {
|
||||
return;
|
||||
@@ -122,7 +118,7 @@ const TopControls = ({
|
||||
<div className="absolute left-0 w-full top-0 z-[100] pointer-events-none">
|
||||
<div className="flex justify-center mt-[0.5rem]">
|
||||
<SegmentedControl
|
||||
data={createViewOptions(currentView, switchingTo, isToolSelected)}
|
||||
data={createViewOptions(currentView, switchingTo)}
|
||||
value={currentView}
|
||||
onChange={handleViewChange}
|
||||
color="blue"
|
||||
|
||||
@@ -33,8 +33,11 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
const { getHomeNavigation } = useSidebarNavigation();
|
||||
|
||||
// Determine if the indicator should be visible (do not require selectedTool to be resolved yet)
|
||||
// Special case: multiTool should always show even when sidebars are hidden
|
||||
const indicatorShouldShow = Boolean(
|
||||
selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)
|
||||
selectedToolKey &&
|
||||
((leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)) ||
|
||||
selectedToolKey === 'multiTool')
|
||||
);
|
||||
|
||||
// Local animation and hover state
|
||||
@@ -47,7 +50,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
const animTimeoutRef = useRef<number | null>(null);
|
||||
const replayRafRef = useRef<number | null>(null);
|
||||
|
||||
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey };
|
||||
const isSwitchingToNewTool = () => { return prevKeyRef.current && prevKeyRef.current !== selectedToolKey; };
|
||||
|
||||
const clearTimers = () => {
|
||||
if (collapseTimeoutRef.current) {
|
||||
@@ -78,7 +81,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
setReplayAnim(false);
|
||||
animTimeoutRef.current = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const firstShow = () => {
|
||||
clearTimers();
|
||||
@@ -88,7 +91,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
animTimeoutRef.current = window.setTimeout(() => {
|
||||
animTimeoutRef.current = null;
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerCollapse = () => {
|
||||
clearTimers();
|
||||
@@ -98,7 +101,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
|
||||
prevKeyRef.current = null;
|
||||
collapseTimeoutRef.current = null;
|
||||
}, 500); // match CSS transition duration
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (indicatorShouldShow) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const isNavButtonActive = (
|
||||
isFilesModalOpen: boolean,
|
||||
configModalOpen: boolean,
|
||||
selectedToolKey?: string | null,
|
||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
||||
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||
): boolean => {
|
||||
const isActiveByLocalState = config.type === 'navigation' && activeButton === config.id;
|
||||
const isActiveByContext =
|
||||
@@ -35,7 +35,7 @@ export const getNavButtonStyle = (
|
||||
isFilesModalOpen: boolean,
|
||||
configModalOpen: boolean,
|
||||
selectedToolKey?: string | null,
|
||||
leftPanelView?: 'toolPicker' | 'toolContent'
|
||||
leftPanelView?: 'toolPicker' | 'toolContent' | 'hidden'
|
||||
) => {
|
||||
const isActive = isNavButtonActive(
|
||||
config,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { useToast } from './ToastContext';
|
||||
import { ToastInstance, ToastLocation } from './types';
|
||||
import { LocalIcon } from '../shared/LocalIcon';
|
||||
@@ -66,7 +65,7 @@ export default function ToastRenderer() {
|
||||
<LocalIcon icon={`material-symbols:${getDefaultIconName(t)}`} width={20} height={20} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Title + count badge */}
|
||||
<div className="toast-title-container">
|
||||
<span>{t.title}</span>
|
||||
@@ -74,7 +73,7 @@ export default function ToastRenderer() {
|
||||
<span className="toast-count-badge">{t.count}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Controls */}
|
||||
<div className="toast-controls">
|
||||
{t.expandable && (
|
||||
@@ -101,20 +100,20 @@ export default function ToastRenderer() {
|
||||
{/* Progress bar - always show when present */}
|
||||
{typeof t.progress === 'number' && (
|
||||
<div className="toast-progress-container">
|
||||
<div
|
||||
<div
|
||||
className={getProgressBarClass(t)}
|
||||
style={{ width: `${t.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Body content - only show when expanded */}
|
||||
{(t.isExpanded || !t.expandable) && (
|
||||
<div className="toast-body">
|
||||
{t.body}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Button - always show when present, positioned below body */}
|
||||
{t.buttonText && t.buttonCallback && (
|
||||
<div className="toast-action-container">
|
||||
|
||||
@@ -10,5 +10,5 @@ export default function ToolLoadingFallback({ toolName }: { toolName?: string })
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import ToolSearch from './toolPicker/ToolSearch';
|
||||
import { useSidebarContext } from "../../contexts/SidebarContext";
|
||||
import rainbowStyles from '../../styles/rainbow.module.css';
|
||||
import { ScrollArea } from '@mantine/core';
|
||||
import { ToolId } from '../../types/toolId';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
|
||||
// No props needed - component uses context
|
||||
|
||||
@@ -14,6 +16,7 @@ export default function ToolPanel() {
|
||||
const { isRainbowMode } = useRainbowThemeContext();
|
||||
const { sidebarRefs } = useSidebarContext();
|
||||
const { toolPanelRef } = sidebarRefs;
|
||||
const isMobile = useMediaQuery('(max-width: 1024px)');
|
||||
|
||||
|
||||
// Use context-based hooks to eliminate prop drilling
|
||||
@@ -33,17 +36,17 @@ export default function ToolPanel() {
|
||||
<div
|
||||
ref={toolPanelRef}
|
||||
data-sidebar="tool-panel"
|
||||
className={`h-screen flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
|
||||
isRainbowMode ? rainbowStyles.rainbowPaper : ''
|
||||
}`}
|
||||
} ${isMobile ? 'h-full border-r-0' : 'h-screen'}`}
|
||||
style={{
|
||||
width: isPanelVisible ? '18.5rem' : '0',
|
||||
width: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
|
||||
padding: '0'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
opacity: isPanelVisible ? 1 : 0,
|
||||
opacity: isMobile || isPanelVisible ? 1 : 0,
|
||||
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
@@ -71,7 +74,7 @@ export default function ToolPanel() {
|
||||
<div className="flex-1 flex flex-col overflow-y-auto">
|
||||
<SearchResults
|
||||
filteredTools={filteredTools}
|
||||
onSelect={handleToolSelect}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
</div>
|
||||
@@ -80,7 +83,7 @@ export default function ToolPanel() {
|
||||
<div className="flex-1 flex flex-col overflow-auto">
|
||||
<ToolPicker
|
||||
selectedToolKey={selectedToolKey}
|
||||
onSelect={handleToolSelect}
|
||||
onSelect={(id) => handleToolSelect(id as ToolId)}
|
||||
filteredTools={filteredTools}
|
||||
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { Suspense } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { useToolWorkflow } from "../../contexts/ToolWorkflowContext";
|
||||
import { BaseToolProps } from "../../types/tool";
|
||||
import ToolLoadingFallback from "./ToolLoadingFallback";
|
||||
@@ -26,7 +26,7 @@ const ToolRenderer = ({
|
||||
|
||||
// Wrap lazy-loaded component with Suspense
|
||||
return (
|
||||
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
|
||||
<Suspense fallback={<ToolLoadingFallback toolName={selectedTool.name} />}>
|
||||
<ToolComponent
|
||||
onPreviewFile={onPreviewFile}
|
||||
onComplete={onComplete}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/* PageNumberPreview.module.css - EXACT copy from StampPreview */
|
||||
|
||||
/* Container styles */
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.containerWithThumbnail {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.containerWithoutThumbnail {
|
||||
background-color: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.containerBorder {
|
||||
border: 1px solid var(--border-default, #333);
|
||||
}
|
||||
|
||||
/* Page thumbnail styles */
|
||||
.pageThumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
filter: grayscale(10%) contrast(95%) brightness(105%);
|
||||
}
|
||||
|
||||
/* Quick grid overlay styles - EXACT copy from stamp */
|
||||
.quickGrid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.gridTile {
|
||||
border: 1px dashed rgba(0, 0, 0, 0.15);
|
||||
background-color: transparent;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 20px;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Position numbers at edges within each tile with extra top/bottom spacing */
|
||||
.gridTile:nth-child(1) { align-items: flex-start; justify-content: flex-start; padding-top: 4px; } /* top-left */
|
||||
.gridTile:nth-child(2) { align-items: flex-start; justify-content: center; padding-top: 4px; } /* top-center */
|
||||
.gridTile:nth-child(3) { align-items: flex-start; justify-content: flex-end; padding-top: 4px; } /* top-right */
|
||||
.gridTile:nth-child(4) { align-items: center; justify-content: flex-start; } /* middle-left */
|
||||
.gridTile:nth-child(5) { align-items: center; justify-content: center; } /* center */
|
||||
.gridTile:nth-child(6) { align-items: center; justify-content: flex-end; } /* middle-right */
|
||||
.gridTile:nth-child(7) { align-items: flex-end; justify-content: flex-start; padding-bottom: 4px; } /* bottom-left */
|
||||
.gridTile:nth-child(8) { align-items: flex-end; justify-content: center; padding-bottom: 4px; } /* bottom-center */
|
||||
.gridTile:nth-child(9) { align-items: flex-end; justify-content: flex-end; padding-bottom: 4px; } /* bottom-right */
|
||||
|
||||
/* Base padding for all tiles */
|
||||
.gridTile {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.gridTileSelected,
|
||||
.gridTileHovered {
|
||||
border: 2px solid var(--mantine-primary-color-filled, #3b82f6);
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
/* Preview header */
|
||||
.previewHeader {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background-color: var(--border-default, #333);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.previewLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Preview disclaimer */
|
||||
.previewDisclaimer {
|
||||
margin-top: 8px;
|
||||
opacity: 0.7;
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AddPageNumbersParameters } from './useAddPageNumbersParameters';
|
||||
import { pdfWorkerManager } from '../../../services/pdfWorkerManager';
|
||||
import { useThumbnailGeneration } from '../../../hooks/useThumbnailGeneration';
|
||||
import styles from './PageNumberPreview.module.css';
|
||||
|
||||
// Simple utilities for page numbers (adapted from stamp)
|
||||
const A4_ASPECT_RATIO = 0.707;
|
||||
|
||||
const getFirstSelectedPage = (input: string): number => {
|
||||
if (!input) return 1;
|
||||
const parts = input.split(',').map(s => s.trim()).filter(Boolean);
|
||||
for (const part of parts) {
|
||||
if (/^\d+\s*-\s*\d+$/.test(part)) {
|
||||
const low = parseInt(part.split('-')[0].trim(), 10);
|
||||
if (Number.isFinite(low) && low > 0) return low;
|
||||
}
|
||||
const n = parseInt(part, 10);
|
||||
if (Number.isFinite(n) && n > 0) return n;
|
||||
}
|
||||
return 1;
|
||||
};
|
||||
|
||||
|
||||
const detectOverallBackgroundColor = async (thumbnailSrc: string | null): Promise<'light' | 'dark'> => {
|
||||
if (!thumbnailSrc) {
|
||||
return 'light'; // Default to light background if no thumbnail
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
resolve('light');
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
// Sample the entire image at reduced resolution for performance
|
||||
const sampleWidth = Math.min(100, img.width);
|
||||
const sampleHeight = Math.min(100, img.height);
|
||||
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||
const data = imageData.data;
|
||||
|
||||
let totalBrightness = 0;
|
||||
let pixelCount = 0;
|
||||
|
||||
// Sample every nth pixel for performance
|
||||
const step = Math.max(1, Math.floor((img.width * img.height) / (sampleWidth * sampleHeight)));
|
||||
|
||||
for (let i = 0; i < data.length; i += 4 * step) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
|
||||
// Calculate perceived brightness using luminance formula
|
||||
const brightness = (0.299 * r + 0.587 * g + 0.114 * b);
|
||||
totalBrightness += brightness;
|
||||
pixelCount++;
|
||||
}
|
||||
|
||||
const averageBrightness = totalBrightness / pixelCount;
|
||||
|
||||
// Threshold: 128 is middle gray
|
||||
resolve(averageBrightness > 128 ? 'light' : 'dark');
|
||||
} catch (error) {
|
||||
console.warn('Error detecting background color:', error);
|
||||
resolve('light'); // Default fallback
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => resolve('light');
|
||||
img.src = thumbnailSrc;
|
||||
});
|
||||
};
|
||||
|
||||
type Props = {
|
||||
parameters: AddPageNumbersParameters;
|
||||
onParameterChange: <K extends keyof AddPageNumbersParameters>(key: K, value: AddPageNumbersParameters[K]) => void;
|
||||
file?: File | null;
|
||||
showQuickGrid?: boolean;
|
||||
};
|
||||
|
||||
export default function PageNumberPreview({ parameters, onParameterChange, file, showQuickGrid }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [, setContainerSize] = useState<{ width: number; height: number }>({ width: 0, height: 0 });
|
||||
const [pageSize, setPageSize] = useState<{ widthPts: number; heightPts: number } | null>(null);
|
||||
const [pageThumbnail, setPageThumbnail] = useState<string | null>(null);
|
||||
const { requestThumbnail } = useThumbnailGeneration();
|
||||
const [hoverTile, setHoverTile] = useState<number | null>(null);
|
||||
const [textColor, setTextColor] = useState<string>('#fff');
|
||||
|
||||
// Observe container size for responsive positioning
|
||||
useEffect(() => {
|
||||
const node = containerRef.current;
|
||||
if (!node) return;
|
||||
const resize = () => {
|
||||
const aspect = pageSize ? (pageSize.widthPts / pageSize.heightPts) : A4_ASPECT_RATIO;
|
||||
setContainerSize({ width: node.clientWidth, height: node.clientWidth / aspect });
|
||||
};
|
||||
resize();
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(node);
|
||||
return () => ro.disconnect();
|
||||
}, [pageSize]);
|
||||
|
||||
// Load first PDF page size in points for accurate scaling
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
setPageSize(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const pdf = await pdfWorkerManager.createDocument(buffer, { disableAutoFetch: true, disableStream: true });
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1 });
|
||||
if (!cancelled) {
|
||||
setPageSize({ widthPts: viewport.width, heightPts: viewport.height });
|
||||
}
|
||||
pdfWorkerManager.destroyDocument(pdf);
|
||||
} catch {
|
||||
if (!cancelled) setPageSize(null);
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => { cancelled = true; };
|
||||
}, [file]);
|
||||
|
||||
// Load first-page thumbnail for background preview
|
||||
useEffect(() => {
|
||||
let isActive = true;
|
||||
const loadThumb = async () => {
|
||||
if (!file || file.type !== 'application/pdf') {
|
||||
setPageThumbnail(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const pageNumber = Math.max(1, getFirstSelectedPage(parameters.pagesToNumber || '1'));
|
||||
const pageId = `${file.name}:${file.size}:${file.lastModified}:page:${pageNumber}`;
|
||||
const thumb = await requestThumbnail(pageId, file, pageNumber);
|
||||
if (isActive) setPageThumbnail(thumb || null);
|
||||
} catch {
|
||||
if (isActive) setPageThumbnail(null);
|
||||
}
|
||||
};
|
||||
loadThumb();
|
||||
return () => { isActive = false; };
|
||||
}, [file, parameters.pagesToNumber, requestThumbnail]);
|
||||
|
||||
// Detect text color based on overall PDF background
|
||||
useEffect(() => {
|
||||
if (!pageThumbnail) {
|
||||
setTextColor('#fff'); // Default to white for no thumbnail
|
||||
return;
|
||||
}
|
||||
|
||||
const detectColor = async () => {
|
||||
const backgroundType = await detectOverallBackgroundColor(pageThumbnail);
|
||||
setTextColor(backgroundType === 'light' ? '#000' : '#fff');
|
||||
};
|
||||
|
||||
detectColor();
|
||||
}, [pageThumbnail]);
|
||||
|
||||
const containerStyle = useMemo(() => ({
|
||||
position: 'relative' as const,
|
||||
width: '100%',
|
||||
aspectRatio: `${(pageSize?.widthPts ?? 595.28) / (pageSize?.heightPts ?? 841.89)} / 1`,
|
||||
backgroundColor: pageThumbnail ? 'transparent' : 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid var(--border-default, #333)',
|
||||
overflow: 'hidden' as const
|
||||
}), [pageSize, pageThumbnail]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.previewHeader}>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.previewLabel}>{t('addPageNumbers.preview', 'Preview Page Numbers')}</div>
|
||||
</div>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`${styles.container} ${styles.containerBorder} ${pageThumbnail ? styles.containerWithThumbnail : styles.containerWithoutThumbnail}`}
|
||||
style={containerStyle}
|
||||
>
|
||||
{pageThumbnail && (
|
||||
<img
|
||||
src={pageThumbnail}
|
||||
alt="page preview"
|
||||
className={styles.pageThumbnail}
|
||||
draggable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick position overlay grid - EXACT copy from stamp */}
|
||||
{showQuickGrid && (
|
||||
<div className={styles.quickGrid}>
|
||||
{Array.from({ length: 9 }).map((_, i) => {
|
||||
const idx = (i + 1) as 1|2|3|4|5|6|7|8|9;
|
||||
const selected = parameters.position === idx;
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
className={`${styles.gridTile} ${selected || hoverTile === idx ? styles.gridTileSelected : ''} ${hoverTile === idx ? styles.gridTileHovered : ''}`}
|
||||
onClick={() => onParameterChange('position', idx as any)}
|
||||
onMouseEnter={() => setHoverTile(idx)}
|
||||
onMouseLeave={() => setHoverTile(null)}
|
||||
style={{
|
||||
color: textColor,
|
||||
textShadow: textColor === '#fff'
|
||||
? '1px 1px 2px rgba(0, 0, 0, 0.8)'
|
||||
: '1px 1px 2px rgba(255, 255, 255, 0.8)'
|
||||
}}
|
||||
>
|
||||
{idx}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.previewDisclaimer}>
|
||||
{t('addPageNumbers.previewDisclaimer', 'Preview is approximate. Final output may vary due to PDF font metrics.')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ToolType, useToolOperation } from '../../../hooks/tools/shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { AddPageNumbersParameters, defaultParameters } from './useAddPageNumbersParameters';
|
||||
|
||||
export const buildAddPageNumbersFormData = (parameters: AddPageNumbersParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append('fileInput', file);
|
||||
formData.append('customMargin', parameters.customMargin);
|
||||
formData.append('position', String(parameters.position));
|
||||
formData.append('fontSize', String(parameters.fontSize));
|
||||
formData.append('fontType', parameters.fontType);
|
||||
formData.append('startingNumber', String(parameters.startingNumber));
|
||||
formData.append('pagesToNumber', parameters.pagesToNumber);
|
||||
formData.append('customText', parameters.customText);
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
export const addPageNumbersOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildAddPageNumbersFormData,
|
||||
operationType: 'addPageNumbers',
|
||||
endpoint: '/api/v1/misc/add-page-numbers',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useAddPageNumbersOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<AddPageNumbersParameters>({
|
||||
...addPageNumbersOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(
|
||||
t('addPageNumbers.error.failed', 'An error occurred while adding page numbers to the PDF.')
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, type BaseParametersHook } from '../../../hooks/tools/shared/useBaseParameters';
|
||||
|
||||
export interface AddPageNumbersParameters extends BaseParameters {
|
||||
customMargin: 'small' | 'medium' | 'large' | 'x-large';
|
||||
position: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
fontSize: number;
|
||||
fontType: 'Times' | 'Helvetica' | 'Courier';
|
||||
startingNumber: number;
|
||||
pagesToNumber: string;
|
||||
customText: string;
|
||||
}
|
||||
|
||||
export const defaultParameters: AddPageNumbersParameters = {
|
||||
customMargin: 'medium',
|
||||
position: 8, // Default to bottom center like the original HTML
|
||||
fontSize: 12,
|
||||
fontType: 'Times',
|
||||
startingNumber: 1,
|
||||
pagesToNumber: '',
|
||||
customText: '',
|
||||
};
|
||||
|
||||
export type AddPageNumbersParametersHook = BaseParametersHook<AddPageNumbersParameters>;
|
||||
|
||||
export const useAddPageNumbersParameters = (): AddPageNumbersParametersHook => {
|
||||
return useBaseParameters<AddPageNumbersParameters>({
|
||||
defaultParameters,
|
||||
endpointName: 'add-page-numbers',
|
||||
validateFn: (params): boolean => {
|
||||
return params.fontSize > 0 && params.startingNumber > 0;
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* AddWatermarkSingleStepSettings - Used for automation only
|
||||
*
|
||||
*
|
||||
* This component combines all watermark settings into a single step interface
|
||||
* for use in the automation system. It includes type selection and all relevant
|
||||
* settings in one unified component.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
import WatermarkTypeSettings from "./WatermarkTypeSettings";
|
||||
@@ -67,4 +66,4 @@ const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disable
|
||||
);
|
||||
};
|
||||
|
||||
export default AddWatermarkSingleStepSettings;
|
||||
export default AddWatermarkSingleStepSettings;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Checkbox, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
@@ -80,4 +79,4 @@ const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkFormatting;
|
||||
export default WatermarkFormatting;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
@@ -60,4 +59,4 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
|
||||
);
|
||||
};
|
||||
|
||||
export default WatermarkStyleSettings;
|
||||
export default WatermarkStyleSettings;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select, ColorInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
|
||||
@@ -27,7 +26,7 @@ const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }:
|
||||
format="hex"
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" fw={500}>
|
||||
{t("watermark.settings.alphabet", "Alphabet")}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Title, Stack, Divider } from "@mantine/core";
|
||||
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
|
||||
@@ -19,11 +18,11 @@ interface AutomationSelectionProps {
|
||||
toolRegistry: Record<string, ToolRegistryEntry>;
|
||||
}
|
||||
|
||||
export default function AutomationSelection({
|
||||
export default function AutomationSelection({
|
||||
savedAutomations,
|
||||
onCreateNew,
|
||||
onRun,
|
||||
onEdit,
|
||||
onCreateNew,
|
||||
onRun,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onCopyFromSuggested,
|
||||
toolRegistry
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, Text, Divider, Collapse, Button, NumberInput } from "@mantine/core";
|
||||
import { BookletImpositionParameters } from "../../../hooks/tools/bookletImposition/useBookletImpositionParameters";
|
||||
@@ -176,4 +176,4 @@ const BookletImpositionSettings = ({ parameters, onParameterChange, disabled = f
|
||||
);
|
||||
};
|
||||
|
||||
export default BookletImpositionSettings;
|
||||
export default BookletImpositionSettings;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Stack, Text, NumberInput, Select, Divider } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CompressParameters } from "../../../hooks/tools/compress/useCompressParameters";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, NumberInput, Checkbox } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select, Switch } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { COLOR_TYPES, FIT_OPTIONS } from "../../../constants/convertConstants";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, NumberInput, Slider } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select, NumberInput, Group } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { COLOR_TYPES, OUTPUT_OPTIONS } from "../../../constants/convertConstants";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text, Select, Alert } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConvertParameters } from '../../../hooks/tools/convert/useConvertParameters';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { Stack, Text, Group, Button, Box, Popover, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
@@ -34,14 +34,14 @@ const GroupedFormatDropdown = ({
|
||||
|
||||
const groupedOptions = useMemo(() => {
|
||||
const groups: Record<string, FormatOption[]> = {};
|
||||
|
||||
|
||||
options.forEach(option => {
|
||||
if (!groups[option.group]) {
|
||||
groups[option.group] = [];
|
||||
}
|
||||
groups[option.group].push(option);
|
||||
});
|
||||
|
||||
|
||||
return groups;
|
||||
}, [options]);
|
||||
|
||||
@@ -77,14 +77,14 @@ const GroupedFormatDropdown = ({
|
||||
padding: '0.5rem 0.75rem',
|
||||
border: `0.0625rem solid ${theme.colors.gray[4]}`,
|
||||
borderRadius: theme.radius.sm,
|
||||
backgroundColor: disabled
|
||||
? theme.colors.gray[1]
|
||||
: colorScheme === 'dark'
|
||||
? theme.colors.dark[6]
|
||||
backgroundColor: disabled
|
||||
? theme.colors.gray[1]
|
||||
: colorScheme === 'dark'
|
||||
? theme.colors.dark[6]
|
||||
: theme.white,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
width: '100%',
|
||||
color: disabled
|
||||
color: disabled
|
||||
? colorScheme === 'dark' ? theme.colors.dark[1] : theme.colors.dark[7]
|
||||
: colorScheme === 'dark' ? theme.colors.dark[0] : theme.colors.dark[9]
|
||||
}}
|
||||
@@ -93,19 +93,19 @@ const GroupedFormatDropdown = ({
|
||||
<Text size="sm" c={value ? undefined : 'dimmed'}>
|
||||
{selectedLabel}
|
||||
</Text>
|
||||
<KeyboardArrowDownIcon
|
||||
style={{
|
||||
<KeyboardArrowDownIcon
|
||||
style={{
|
||||
fontSize: '1rem',
|
||||
transform: dropdownOpened ? 'rotate(180deg)' : 'rotate(0deg)',
|
||||
transition: 'transform 0.2s ease',
|
||||
color: colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6]
|
||||
}}
|
||||
}}
|
||||
/>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown
|
||||
style={{
|
||||
<Popover.Dropdown
|
||||
style={{
|
||||
minWidth: Math.min(350, parseInt(minWidth.replace('rem', '')) * 16),
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '40vh',
|
||||
@@ -117,10 +117,10 @@ const GroupedFormatDropdown = ({
|
||||
<Stack gap="md">
|
||||
{Object.entries(groupedOptions).map(([groupName, groupOptions]) => (
|
||||
<Box key={groupName}>
|
||||
<Text
|
||||
size="sm"
|
||||
fw={600}
|
||||
c={colorScheme === 'dark' ? 'dark.2' : 'gray.6'}
|
||||
<Text
|
||||
size="sm"
|
||||
fw={600}
|
||||
c={colorScheme === 'dark' ? 'dark.2' : 'gray.6'}
|
||||
mb="xs"
|
||||
>
|
||||
{groupName}
|
||||
@@ -153,4 +153,4 @@ const GroupedFormatDropdown = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupedFormatDropdown;
|
||||
export default GroupedFormatDropdown;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { Divider, Select, Stack, Switch } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PageLayoutParameters } from '../../../hooks/tools/pageLayout/usePageLayoutParameters';
|
||||
import { getPagesPerSheetOptions } from './constants';
|
||||
|
||||
export default function PageLayoutSettings({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled,
|
||||
}: {
|
||||
parameters: PageLayoutParameters;
|
||||
onParameterChange: <K extends keyof PageLayoutParameters>(
|
||||
key: K,
|
||||
value: PageLayoutParameters[K]
|
||||
) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const options = getPagesPerSheetOptions(t);
|
||||
const selected = options.find((o) => o.value === parameters.pagesPerSheet) || options[0];
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
<Select
|
||||
label={t('pageLayout.pagesPerSheet', 'Pages per sheet:')}
|
||||
data={options.map(o => ({ value: String(o.value), label: o.label }))}
|
||||
value={String(parameters.pagesPerSheet)}
|
||||
onChange={(v) => onParameterChange('pagesPerSheet', Number(v))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
{selected && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--information-text-bg)',
|
||||
color: 'var(--information-text-color)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '8px',
|
||||
marginTop: '4px',
|
||||
fontSize: '0.75rem',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{selected.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Switch
|
||||
checked={parameters.addBorder}
|
||||
onChange={(e) => onParameterChange('addBorder', e.currentTarget.checked)}
|
||||
label={t('pageLayout.addBorder', 'Add Borders')}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export type PagesPerSheetOption = {
|
||||
value: number;
|
||||
label: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export const getPagesPerSheetOptions = (t: TFunction): PagesPerSheetOption[] => [
|
||||
{
|
||||
value: 2,
|
||||
label: '2',
|
||||
description: t('pageLayout.desc.2', 'Place 2 pages side-by-side on a single sheet.')
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: '3',
|
||||
description: t('pageLayout.desc.3', 'Place 3 pages on a single sheet in a single row.')
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
label: '4',
|
||||
description: t('pageLayout.desc.4', 'Place 4 pages on a single sheet (2 × 2 grid).')
|
||||
},
|
||||
{
|
||||
value: 9,
|
||||
label: '9',
|
||||
description: t('pageLayout.desc.9', 'Place 9 pages on a single sheet (3 × 3 grid).')
|
||||
},
|
||||
{
|
||||
value: 16,
|
||||
label: '16',
|
||||
description: t('pageLayout.desc.16', 'Place 16 pages on a single sheet (4 × 4 grid).')
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Stack, Text, Alert } from '@mantine/core';
|
||||
import LocalIcon from '../../shared/LocalIcon';
|
||||
|
||||
const RemoveAnnotationsSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Alert
|
||||
icon={<LocalIcon icon="info-rounded" width="1.2rem" height="1.2rem" />}
|
||||
title={t('removeAnnotations.info.title', 'About Remove Annotations')}
|
||||
color="blue"
|
||||
variant="light"
|
||||
>
|
||||
<Text size="sm">
|
||||
{t('removeAnnotations.info.description',
|
||||
'This tool will remove all annotations (comments, highlights, notes, etc.) from your PDF documents.'
|
||||
)}
|
||||
</Text>
|
||||
</Alert>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default RemoveAnnotationsSettings;
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Divider, Select, Stack, TextInput } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReorganizePagesParameters } from '../../../hooks/tools/reorganizePages/useReorganizePagesParameters';
|
||||
@@ -31,7 +30,7 @@ export default function ReorganizePagesSettings({
|
||||
disabled={disabled}
|
||||
/>
|
||||
{selectedMode && (
|
||||
<div
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--information-text-bg)',
|
||||
color: 'var(--information-text-color)',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { Stack, Text, Select, ColorInput } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ReplaceColorParameters } from "../../../hooks/tools/replaceColor/useReplaceColorParameters";
|
||||
@@ -105,4 +104,4 @@ const ReplaceColorSettings = ({ parameters, onParameterChange, disabled = false
|
||||
);
|
||||
};
|
||||
|
||||
export default ReplaceColorSettings;
|
||||
export default ReplaceColorSettings;
|
||||
|
||||
@@ -30,6 +30,6 @@ const ErrorNotification = ({
|
||||
{error}
|
||||
</Notification>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ErrorNotification;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Stack, Text } from '@mantine/core';
|
||||
import { formatFileSize, getFileDate } from '../../../utils/fileUtils';
|
||||
|
||||
@@ -24,4 +23,4 @@ const FileMetadata = ({ file }: FileMetadataProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default FileMetadata;
|
||||
export default FileMetadata;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Stack, Group, ActionIcon, Text } from '@mantine/core';
|
||||
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
||||
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Stack, Text, NumberInput } from "@mantine/core";
|
||||
|
||||
interface NumberInputWithUnitProps {
|
||||
@@ -11,14 +11,14 @@ interface NumberInputWithUnitProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const NumberInputWithUnit = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
unit,
|
||||
min,
|
||||
max,
|
||||
disabled = false
|
||||
const NumberInputWithUnit = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
unit,
|
||||
min,
|
||||
max,
|
||||
disabled = false
|
||||
}: NumberInputWithUnitProps) => {
|
||||
const [localValue, setLocalValue] = useState<number | string>(value);
|
||||
|
||||
@@ -54,4 +54,4 @@ const NumberInputWithUnit = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default NumberInputWithUnit;
|
||||
export default NumberInputWithUnit;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -52,6 +51,6 @@ const OperationButton = ({
|
||||
}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default OperationButton;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { Box, Text, Loader, Stack, Center, Flex } from '@mantine/core';
|
||||
import FilePreview from '../../shared/FilePreview';
|
||||
import FileMetadata from './FileMetadata';
|
||||
|
||||
@@ -149,7 +149,7 @@ const ToolStep = ({
|
||||
<Divider style={{ color: '#E2E8F0', marginLeft: '1rem', marginRight: '-0.5rem' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ToolStepFactory for creating numbered steps
|
||||
export function createToolSteps() {
|
||||
@@ -164,13 +164,16 @@ export function createToolSteps() {
|
||||
const isVisible = props.isVisible !== false;
|
||||
const currentStepNumber = isVisible ? stepNumber++ : undefined;
|
||||
|
||||
const step = React.createElement(ToolStep, {
|
||||
...props,
|
||||
title,
|
||||
_stepNumber: currentStepNumber,
|
||||
children,
|
||||
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
|
||||
});
|
||||
const step = React.createElement(
|
||||
ToolStep,
|
||||
{
|
||||
...props,
|
||||
title,
|
||||
_stepNumber: currentStepNumber,
|
||||
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
|
||||
},
|
||||
children
|
||||
);
|
||||
|
||||
steps.push(step);
|
||||
return step;
|
||||
@@ -186,9 +189,9 @@ export function createToolSteps() {
|
||||
|
||||
const getVisibleCount = () => {
|
||||
return steps.filter(step => {
|
||||
const props = step.props as ToolStepProps;
|
||||
const isVisible = props.isVisible !== false;
|
||||
const excludeFromCount = props._excludeFromCount === true;
|
||||
const stepProps = step.props as ToolStepProps;
|
||||
const isVisible = stepProps.isVisible !== false;
|
||||
const excludeFromCount = stepProps._excludeFromCount === true;
|
||||
return isVisible && !excludeFromCount;
|
||||
}).length;
|
||||
};
|
||||
@@ -203,9 +206,9 @@ export function ToolStepProvider({ children, forceStepNumbers }: { children: Rea
|
||||
let count = 0;
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (React.isValidElement(child) && child.type === ToolStep) {
|
||||
const props = child.props as ToolStepProps;
|
||||
const isVisible = props.isVisible !== false;
|
||||
const excludeFromCount = props._excludeFromCount === true;
|
||||
const stepProps = child.props as ToolStepProps;
|
||||
const isVisible = stepProps.isVisible !== false;
|
||||
const excludeFromCount = stepProps._excludeFromCount === true;
|
||||
if (isVisible && !excludeFromCount) count++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { TextInput, Combobox, useCombobox } from '@mantine/core';
|
||||
|
||||
interface PenSizeSelectorProps {
|
||||
value: number;
|
||||
inputValue: string;
|
||||
onValueChange: (size: number) => void;
|
||||
onInputChange: (input: string) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
style?: React.CSSProperties;
|
||||
size?: string;
|
||||
}
|
||||
|
||||
const PenSizeSelector = ({
|
||||
value,
|
||||
inputValue,
|
||||
onValueChange,
|
||||
onInputChange,
|
||||
disabled = false,
|
||||
placeholder = "Type or select pen size (1-200)",
|
||||
style,
|
||||
size
|
||||
}: PenSizeSelectorProps) => {
|
||||
const combobox = useCombobox();
|
||||
|
||||
const penSizeOptions = ['1', '2', '3', '4', '5', '8', '10', '12', '15', '20'];
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
onOptionSubmit={(optionValue) => {
|
||||
const penSize = parseInt(optionValue);
|
||||
if (!isNaN(penSize)) {
|
||||
onValueChange(penSize);
|
||||
onInputChange(optionValue);
|
||||
}
|
||||
combobox.closeDropdown();
|
||||
}}
|
||||
store={combobox}
|
||||
withinPortal={false}
|
||||
>
|
||||
<Combobox.Target>
|
||||
<TextInput
|
||||
placeholder={placeholder}
|
||||
size={size}
|
||||
value={inputValue}
|
||||
onChange={(event) => {
|
||||
const inputVal = event.currentTarget.value;
|
||||
onInputChange(inputVal);
|
||||
|
||||
const penSize = parseInt(inputVal);
|
||||
if (!isNaN(penSize) && penSize >= 1 && penSize <= 200) {
|
||||
onValueChange(penSize);
|
||||
}
|
||||
|
||||
combobox.openDropdown();
|
||||
combobox.updateSelectedOptionIndex();
|
||||
}}
|
||||
onClick={() => combobox.openDropdown()}
|
||||
onFocus={() => combobox.openDropdown()}
|
||||
onBlur={() => {
|
||||
combobox.closeDropdown();
|
||||
const penSize = parseInt(inputValue);
|
||||
if (isNaN(penSize) || penSize < 1 || penSize > 200) {
|
||||
onInputChange(value.toString());
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
style={style}
|
||||
/>
|
||||
</Combobox.Target>
|
||||
|
||||
<Combobox.Dropdown>
|
||||
<Combobox.Options>
|
||||
{penSizeOptions.map((sizeOption) => (
|
||||
<Combobox.Option value={sizeOption} key={sizeOption}>
|
||||
{sizeOption}px
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
</Combobox.Dropdown>
|
||||
</Combobox>
|
||||
);
|
||||
};
|
||||
|
||||
export default PenSizeSelector;
|
||||
@@ -0,0 +1,259 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Stack, Button, Text, Alert, Tabs } from '@mantine/core';
|
||||
import { SignParameters } from "../../../hooks/tools/sign/useSignParameters";
|
||||
import { SuggestedToolsSection } from "../shared/SuggestedToolsSection";
|
||||
|
||||
// Import the new reusable components
|
||||
import { DrawingCanvas } from "../../annotation/shared/DrawingCanvas";
|
||||
import { DrawingControls } from "../../annotation/shared/DrawingControls";
|
||||
import { ImageUploader } from "../../annotation/shared/ImageUploader";
|
||||
import { TextInputWithFont } from "../../annotation/shared/TextInputWithFont";
|
||||
import { ColorPicker } from "../../annotation/shared/ColorPicker";
|
||||
|
||||
interface SignSettingsProps {
|
||||
parameters: SignParameters;
|
||||
onParameterChange: <K extends keyof SignParameters>(key: K, value: SignParameters[K]) => void;
|
||||
disabled?: boolean;
|
||||
onActivateDrawMode?: () => void;
|
||||
onActivateSignaturePlacement?: () => void;
|
||||
onDeactivateSignature?: () => void;
|
||||
onUpdateDrawSettings?: (color: string, size: number) => void;
|
||||
onUndo?: () => void;
|
||||
onRedo?: () => void;
|
||||
onSave?: () => void;
|
||||
}
|
||||
|
||||
const SignSettings = ({
|
||||
parameters,
|
||||
onParameterChange,
|
||||
disabled = false,
|
||||
onActivateSignaturePlacement,
|
||||
onDeactivateSignature,
|
||||
onUndo,
|
||||
onRedo,
|
||||
onSave
|
||||
}: SignSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// State for drawing
|
||||
const [selectedColor, setSelectedColor] = useState('#000000');
|
||||
const [penSize, setPenSize] = useState(2);
|
||||
const [penSizeInput, setPenSizeInput] = useState('2');
|
||||
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
|
||||
|
||||
// State for different signature types
|
||||
const [canvasSignatureData, setCanvasSignatureData] = useState<string | null>(null);
|
||||
const [imageSignatureData, setImageSignatureData] = useState<string | null>(null);
|
||||
|
||||
// Handle image upload
|
||||
const handleImageChange = async (file: File | null) => {
|
||||
if (file && !disabled) {
|
||||
try {
|
||||
const result = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target?.result) {
|
||||
resolve(e.target.result as string);
|
||||
} else {
|
||||
reject(new Error('Failed to read file'));
|
||||
}
|
||||
};
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Clear any existing canvas signatures when uploading image
|
||||
setCanvasSignatureData(null);
|
||||
setImageSignatureData(result);
|
||||
} catch (error) {
|
||||
console.error('Error reading file:', error);
|
||||
}
|
||||
} else if (!file) {
|
||||
setImageSignatureData(null);
|
||||
if (onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle signature data changes
|
||||
const handleCanvasSignatureChange = (data: string | null) => {
|
||||
setCanvasSignatureData(prev => {
|
||||
if (prev === data) return prev; // Prevent unnecessary updates
|
||||
return data;
|
||||
});
|
||||
if (data) {
|
||||
// Clear image data when canvas is used
|
||||
setImageSignatureData(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle signature mode deactivation when switching types
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType !== 'text' && onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
}
|
||||
}, [parameters.signatureType]);
|
||||
|
||||
// Handle text signature activation
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'text' && parameters.signerName && parameters.signerName.trim() !== '') {
|
||||
if (onActivateSignaturePlacement) {
|
||||
setTimeout(() => {
|
||||
onActivateSignaturePlacement();
|
||||
}, 100);
|
||||
}
|
||||
} else if (parameters.signatureType === 'text' && (!parameters.signerName || parameters.signerName.trim() === '')) {
|
||||
if (onDeactivateSignature) {
|
||||
onDeactivateSignature();
|
||||
}
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signerName, onActivateSignaturePlacement, onDeactivateSignature]);
|
||||
|
||||
// Handle signature data updates
|
||||
useEffect(() => {
|
||||
let newSignatureData: string | undefined = undefined;
|
||||
|
||||
if (parameters.signatureType === 'image' && imageSignatureData) {
|
||||
newSignatureData = imageSignatureData;
|
||||
} else if (parameters.signatureType === 'canvas' && canvasSignatureData) {
|
||||
newSignatureData = canvasSignatureData;
|
||||
}
|
||||
|
||||
// Only update if the signature data has actually changed
|
||||
if (parameters.signatureData !== newSignatureData) {
|
||||
onParameterChange('signatureData', newSignatureData);
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signatureData, canvasSignatureData, imageSignatureData]);
|
||||
|
||||
// Handle image signature activation - activate when image data syncs with parameters
|
||||
useEffect(() => {
|
||||
if (parameters.signatureType === 'image' && imageSignatureData && parameters.signatureData === imageSignatureData && onActivateSignaturePlacement) {
|
||||
setTimeout(() => {
|
||||
onActivateSignaturePlacement();
|
||||
}, 100);
|
||||
}
|
||||
}, [parameters.signatureType, parameters.signatureData, imageSignatureData]);
|
||||
|
||||
// Draw settings are no longer needed since draw mode is removed
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{/* Signature Type Selection */}
|
||||
<Tabs
|
||||
value={parameters.signatureType}
|
||||
onChange={(value) => onParameterChange('signatureType', value as 'image' | 'text' | 'canvas')}
|
||||
>
|
||||
<Tabs.List grow>
|
||||
<Tabs.Tab value="canvas" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.canvas', 'Canvas')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="image" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.image', 'Image')}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="text" style={{ fontSize: '0.8rem' }}>
|
||||
{t('sign.type.text', 'Text')}
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
|
||||
{/* Drawing Controls */}
|
||||
<DrawingControls
|
||||
onUndo={onUndo}
|
||||
onRedo={onRedo}
|
||||
onPlaceSignature={() => {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
}}
|
||||
hasSignatureData={!!(canvasSignatureData || imageSignatureData || (parameters.signerName && parameters.signerName.trim() !== ''))}
|
||||
disabled={disabled}
|
||||
showPlaceButton={false}
|
||||
placeButtonText="Update and Place"
|
||||
/>
|
||||
|
||||
{/* Signature Creation based on type */}
|
||||
{parameters.signatureType === 'canvas' && (
|
||||
<DrawingCanvas
|
||||
selectedColor={selectedColor}
|
||||
penSize={penSize}
|
||||
penSizeInput={penSizeInput}
|
||||
onColorSwatchClick={() => setIsColorPickerOpen(true)}
|
||||
onPenSizeChange={setPenSize}
|
||||
onPenSizeInputChange={setPenSizeInput}
|
||||
onSignatureDataChange={handleCanvasSignatureChange}
|
||||
disabled={disabled}
|
||||
additionalButtons={
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onActivateSignaturePlacement) {
|
||||
onActivateSignaturePlacement();
|
||||
}
|
||||
}}
|
||||
color="blue"
|
||||
variant="filled"
|
||||
disabled={disabled || !canvasSignatureData}
|
||||
>
|
||||
Update and Place
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parameters.signatureType === 'image' && (
|
||||
<ImageUploader
|
||||
onImageChange={handleImageChange}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
{parameters.signatureType === 'text' && (
|
||||
<TextInputWithFont
|
||||
text={parameters.signerName || ''}
|
||||
onTextChange={(text) => onParameterChange('signerName', text)}
|
||||
fontSize={parameters.fontSize || 16}
|
||||
onFontSizeChange={(size) => onParameterChange('fontSize', size)}
|
||||
fontFamily={parameters.fontFamily || 'Helvetica'}
|
||||
onFontFamilyChange={(family) => onParameterChange('fontFamily', family)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Instructions for placing signature */}
|
||||
<Alert color="blue" title={t('sign.instructions.title', 'How to add signature')}>
|
||||
<Text size="sm">
|
||||
{parameters.signatureType === 'canvas' && 'After drawing your signature in the canvas above, click "Update and Place" then click anywhere on the PDF to place it.'}
|
||||
{parameters.signatureType === 'image' && 'After uploading your signature image above, click anywhere on the PDF to place it.'}
|
||||
{parameters.signatureType === 'text' && 'After entering your name above, click anywhere on the PDF to place your signature.'}
|
||||
</Text>
|
||||
</Alert>
|
||||
|
||||
{/* Color Picker Modal */}
|
||||
<ColorPicker
|
||||
isOpen={isColorPickerOpen}
|
||||
onClose={() => setIsColorPickerOpen(false)}
|
||||
selectedColor={selectedColor}
|
||||
onColorChange={setSelectedColor}
|
||||
/>
|
||||
|
||||
{/* Apply Signatures Button */}
|
||||
{onSave && (
|
||||
<Button
|
||||
onClick={onSave}
|
||||
color="blue"
|
||||
variant="filled"
|
||||
fullWidth
|
||||
>
|
||||
{t('sign.applySignatures', 'Apply Signatures')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Suggested Tools Section */}
|
||||
<SuggestedToolsSection />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignSettings;
|
||||
@@ -158,6 +158,6 @@ const SplitSettings = ({
|
||||
{parameters.method === SPLIT_METHODS.BY_PAGE_DIVIDER && renderByPageDividerForm()}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default SplitSettings;
|
||||
|
||||
@@ -17,7 +17,8 @@ interface ToolButtonProps {
|
||||
}
|
||||
|
||||
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
|
||||
const isUnavailable = !tool.component && !tool.link;
|
||||
// Special case: read and multiTool are navigational tools that are always available
|
||||
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
|
||||
const { getToolNavigation } = useToolNavigation();
|
||||
|
||||
const handleClick = (id: string) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { useState, useRef, useEffect, useMemo } from "react";
|
||||
import { Stack, Button, Text } from "@mantine/core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalIcon from '../../shared/LocalIcon';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useSearch } from '@embedpdf/plugin-search/react';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
import { SEARCH_CONSTANTS } from './constants/search';
|
||||
@@ -24,9 +24,9 @@ interface SearchResultState {
|
||||
activeResultIndex?: number;
|
||||
}
|
||||
|
||||
export function CustomSearchLayer({
|
||||
pageIndex,
|
||||
scale,
|
||||
export function CustomSearchLayer({
|
||||
pageIndex,
|
||||
scale,
|
||||
highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND,
|
||||
activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND,
|
||||
opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY,
|
||||
@@ -42,17 +42,17 @@ export function CustomSearchLayer({
|
||||
if (!searchProvides) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
|
||||
// Auto-scroll to active search result
|
||||
if (state?.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
|
||||
const activeResult = state.results[state.activeResultIndex];
|
||||
if (activeResult) {
|
||||
if (activeResult) {
|
||||
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
|
||||
scrollActions.scrollToPage(pageNumber);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
setSearchResultState(state);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export function CustomSearchLayer({
|
||||
const filtered = searchResultState.results
|
||||
.map((result, originalIndex) => ({ result, originalIndex }))
|
||||
.filter(({ result }) => result.pageIndex === pageIndex);
|
||||
|
||||
|
||||
return filtered;
|
||||
}, [searchResultState, pageIndex]);
|
||||
|
||||
@@ -78,7 +78,7 @@ export function CustomSearchLayer({
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
@@ -117,4 +117,4 @@ export function CustomSearchLayer({
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useViewer } from "../../contexts/ViewerContext";
|
||||
import { LocalEmbedPDF } from './LocalEmbedPDF';
|
||||
import { PdfViewerToolbar } from './PdfViewerToolbar';
|
||||
import { ThumbnailSidebar } from './ThumbnailSidebar';
|
||||
import { useNavigationState } from '../../contexts/NavigationContext';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
|
||||
export interface EmbedPdfViewerProps {
|
||||
sidebarsVisible: boolean;
|
||||
@@ -33,6 +35,12 @@ const EmbedPdfViewerContent = ({
|
||||
const zoomState = getZoomState();
|
||||
const spreadState = getSpreadState();
|
||||
|
||||
// Check if we're in signature mode
|
||||
const { selectedTool } = useNavigationState();
|
||||
const isSignatureMode = selectedTool === 'sign';
|
||||
|
||||
// Get signature context
|
||||
const { signatureApiRef, historyApiRef } = useSignature();
|
||||
|
||||
// Get current file from FileContext
|
||||
const { selectors } = useFileState();
|
||||
@@ -178,6 +186,13 @@ const EmbedPdfViewerContent = ({
|
||||
<LocalEmbedPDF
|
||||
file={effectiveFile.file}
|
||||
url={effectiveFile.url}
|
||||
enableSignature={isSignatureMode}
|
||||
signatureApiRef={signatureApiRef as React.RefObject<any>}
|
||||
historyApiRef={historyApiRef as React.RefObject<any>}
|
||||
onSignatureAdded={() => {
|
||||
// Handle signature added - for debugging, enable console logs as needed
|
||||
// Future: Handle signature completion
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||
import { useHistoryCapability } from '@embedpdf/plugin-history/react';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
import { uuidV4 } from '@embedpdf/models';
|
||||
|
||||
export interface HistoryAPI {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => boolean;
|
||||
canRedo: () => boolean;
|
||||
}
|
||||
|
||||
export const HistoryAPIBridge = forwardRef<HistoryAPI>(function HistoryAPIBridge(_, ref) {
|
||||
const { provides: historyApi } = useHistoryCapability();
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { getImageData, storeImageData } = useSignature();
|
||||
|
||||
// Monitor annotation events to detect when annotations are restored
|
||||
useEffect(() => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
const handleAnnotationEvent = (event: any) => {
|
||||
const annotation = event.annotation;
|
||||
|
||||
// Store image data for all STAMP annotations immediately when created or modified
|
||||
if (annotation && annotation.type === 13 && annotation.id && annotation.imageSrc) {
|
||||
const storedImageData = getImageData(annotation.id);
|
||||
if (!storedImageData || storedImageData !== annotation.imageSrc) {
|
||||
storeImageData(annotation.id, annotation.imageSrc);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle annotation restoration after undo operations
|
||||
if (event.type === 'create' && event.committed) {
|
||||
// Check if this is a STAMP annotation (signature) that might need image data restoration
|
||||
if (annotation && annotation.type === 13 && annotation.id) {
|
||||
getImageData(annotation.id);
|
||||
|
||||
// Delay the check to allow the annotation to be fully created
|
||||
setTimeout(() => {
|
||||
const currentStoredData = getImageData(annotation.id);
|
||||
// Check if the annotation lacks image data but we have it stored
|
||||
if (currentStoredData && (!annotation.imageSrc || annotation.imageSrc !== currentStoredData)) {
|
||||
|
||||
// Generate new ID to avoid React key conflicts
|
||||
const newId = uuidV4();
|
||||
|
||||
// Recreation with stored image data
|
||||
const restoredData = {
|
||||
type: annotation.type,
|
||||
rect: annotation.rect,
|
||||
author: annotation.author || 'Digital Signature',
|
||||
subject: annotation.subject || 'Digital Signature',
|
||||
pageIndex: event.pageIndex,
|
||||
id: newId,
|
||||
created: annotation.created || new Date(),
|
||||
imageSrc: currentStoredData
|
||||
};
|
||||
|
||||
// Update stored data to use new ID
|
||||
storeImageData(newId, currentStoredData);
|
||||
|
||||
// Replace the annotation with one that has proper image data
|
||||
try {
|
||||
annotationApi.deleteAnnotation(event.pageIndex, annotation.id);
|
||||
// Small delay to ensure deletion completes
|
||||
setTimeout(() => {
|
||||
annotationApi.createAnnotation(event.pageIndex, restoredData);
|
||||
}, 50);
|
||||
} catch (error) {
|
||||
console.error('HistoryAPI: Failed to restore annotation:', error);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add the event listener
|
||||
annotationApi.onAnnotationEvent(handleAnnotationEvent);
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
// Note: EmbedPDF doesn't provide a way to remove event listeners
|
||||
// This is a limitation of the current API
|
||||
};
|
||||
}, [annotationApi, getImageData, storeImageData]);
|
||||
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
undo: () => {
|
||||
if (historyApi) {
|
||||
historyApi.undo();
|
||||
}
|
||||
},
|
||||
|
||||
redo: () => {
|
||||
if (historyApi) {
|
||||
historyApi.redo();
|
||||
}
|
||||
},
|
||||
|
||||
canUndo: () => {
|
||||
return historyApi ? historyApi.canUndo() : false;
|
||||
},
|
||||
|
||||
canRedo: () => {
|
||||
return historyApi ? historyApi.canRedo() : false;
|
||||
},
|
||||
}), [historyApi]);
|
||||
|
||||
return null; // This is a bridge component with no UI
|
||||
});
|
||||
|
||||
HistoryAPIBridge.displayName = 'HistoryAPIBridge';
|
||||
@@ -19,6 +19,11 @@ import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
||||
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
||||
import { ExportPluginPackage } from '@embedpdf/plugin-export/react';
|
||||
import { Rotation } from '@embedpdf/models';
|
||||
|
||||
// Import annotation plugins
|
||||
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
|
||||
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react';
|
||||
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||
import ToolLoadingFallback from '../tools/ToolLoadingFallback';
|
||||
@@ -30,15 +35,22 @@ import { SpreadAPIBridge } from './SpreadAPIBridge';
|
||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||
import { SignatureAPIBridge, SignatureAPI } from './SignatureAPIBridge';
|
||||
import { HistoryAPIBridge, HistoryAPI } from './HistoryAPIBridge';
|
||||
import { ExportAPIBridge } from './ExportAPIBridge';
|
||||
|
||||
interface LocalEmbedPDFProps {
|
||||
file?: File | Blob;
|
||||
url?: string | null;
|
||||
enableSignature?: boolean;
|
||||
onSignatureAdded?: (annotation: any) => void;
|
||||
signatureApiRef?: React.RefObject<SignatureAPI>;
|
||||
historyApiRef?: React.RefObject<HistoryAPI>;
|
||||
}
|
||||
|
||||
export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
||||
export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
const [, setAnnotations] = useState<Array<{id: string, pageIndex: number, rect: any}>>([]);
|
||||
|
||||
// Convert File to URL if needed
|
||||
useEffect(() => {
|
||||
@@ -80,6 +92,17 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
||||
// Register selection plugin (depends on InteractionManager)
|
||||
createPluginRegistration(SelectionPluginPackage),
|
||||
|
||||
// Register history plugin for undo/redo (recommended for annotations)
|
||||
...(enableSignature ? [createPluginRegistration(HistoryPluginPackage)] : []),
|
||||
|
||||
// Register annotation plugin (depends on InteractionManager, Selection, History)
|
||||
...(enableSignature ? [createPluginRegistration(AnnotationPluginPackage, {
|
||||
annotationAuthor: 'Digital Signature',
|
||||
autoCommit: true,
|
||||
deactivateToolAfterCreate: false,
|
||||
selectAfterCreate: true,
|
||||
})] : []),
|
||||
|
||||
// Register pan plugin (depends on Viewport, InteractionManager)
|
||||
createPluginRegistration(PanPluginPackage, {
|
||||
defaultMode: 'mobile', // Try mobile mode which might be more permissive
|
||||
@@ -168,7 +191,72 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
||||
minHeight: 0,
|
||||
minWidth: 0
|
||||
}}>
|
||||
<EmbedPDF engine={engine} plugins={plugins}>
|
||||
<EmbedPDF
|
||||
engine={engine}
|
||||
plugins={plugins}
|
||||
onInitialized={enableSignature ? async (registry) => {
|
||||
const annotationPlugin = registry.getPlugin('annotation');
|
||||
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||
|
||||
const annotationApi = annotationPlugin.provides();
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Add custom signature stamp tool for image signatures
|
||||
annotationApi.addTool({
|
||||
id: 'signatureStamp',
|
||||
name: 'Digital Signature',
|
||||
interaction: { exclusive: false, cursor: 'copy' },
|
||||
matchScore: () => 0,
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
// Image will be set dynamically when signature is created
|
||||
},
|
||||
});
|
||||
|
||||
// Add custom ink signature tool for drawn signatures
|
||||
annotationApi.addTool({
|
||||
id: 'signatureInk',
|
||||
name: 'Signature Draw',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: () => 0,
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
color: '#000000',
|
||||
opacity: 1.0,
|
||||
borderWidth: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for annotation events to track annotations and notify parent
|
||||
annotationApi.onAnnotationEvent((event: any) => {
|
||||
if (event.type === 'create' && event.committed) {
|
||||
// Add to annotations list
|
||||
setAnnotations(prev => [...prev, {
|
||||
id: event.annotation.id,
|
||||
pageIndex: event.pageIndex,
|
||||
rect: event.annotation.rect
|
||||
}]);
|
||||
|
||||
|
||||
// Notify parent if callback provided
|
||||
if (onSignatureAdded) {
|
||||
onSignatureAdded(event.annotation);
|
||||
}
|
||||
} else if (event.type === 'delete' && event.committed) {
|
||||
// Remove from annotations list
|
||||
setAnnotations(prev => prev.filter(ann => ann.id !== event.annotation.id));
|
||||
} else if (event.type === 'loaded') {
|
||||
// Handle initial load of annotations
|
||||
const loadedAnnotations = event.annotations || [];
|
||||
setAnnotations(loadedAnnotations.map((ann: any) => ({
|
||||
id: ann.id,
|
||||
pageIndex: ann.pageIndex || 0,
|
||||
rect: ann.rect
|
||||
})));
|
||||
}
|
||||
});
|
||||
} : undefined}
|
||||
>
|
||||
<ZoomAPIBridge />
|
||||
<ScrollAPIBridge />
|
||||
<SelectionAPIBridge />
|
||||
@@ -177,6 +265,8 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
||||
<SearchAPIBridge />
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
|
||||
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
|
||||
<ExportAPIBridge />
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
@@ -221,6 +311,17 @@ export function LocalEmbedPDF({ file, url }: LocalEmbedPDFProps) {
|
||||
|
||||
{/* Selection layer for text interaction */}
|
||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||
{/* Annotation layer for signatures (only when enabled) */}
|
||||
{enableSignature && (
|
||||
<AnnotationLayer
|
||||
pageIndex={pageIndex}
|
||||
scale={scale}
|
||||
pageWidth={width}
|
||||
pageHeight={height}
|
||||
rotation={rotation || 0}
|
||||
selectionOutlineColor="#007ACC"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</PagePointerProvider>
|
||||
</Rotate>
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { createPluginRegistration } from '@embedpdf/core';
|
||||
import { EmbedPDF } from '@embedpdf/core/react';
|
||||
import { usePdfiumEngine } from '@embedpdf/engines/react';
|
||||
|
||||
// Import the essential plugins
|
||||
import { Viewport, ViewportPluginPackage } from '@embedpdf/plugin-viewport/react';
|
||||
import { Scroller, ScrollPluginPackage, ScrollStrategy } from '@embedpdf/plugin-scroll/react';
|
||||
import { LoaderPluginPackage } from '@embedpdf/plugin-loader/react';
|
||||
import { RenderPluginPackage } from '@embedpdf/plugin-render/react';
|
||||
import { ZoomPluginPackage } from '@embedpdf/plugin-zoom/react';
|
||||
import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react';
|
||||
import { SelectionLayer, SelectionPluginPackage } from '@embedpdf/plugin-selection/react';
|
||||
import { TilingLayer, TilingPluginPackage } from '@embedpdf/plugin-tiling/react';
|
||||
import { PanPluginPackage } from '@embedpdf/plugin-pan/react';
|
||||
import { SpreadPluginPackage, SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { SearchPluginPackage } from '@embedpdf/plugin-search/react';
|
||||
import { ThumbnailPluginPackage } from '@embedpdf/plugin-thumbnail/react';
|
||||
import { RotatePluginPackage, Rotate } from '@embedpdf/plugin-rotate/react';
|
||||
import { Rotation } from '@embedpdf/models';
|
||||
|
||||
// Import annotation plugins
|
||||
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
|
||||
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react';
|
||||
import { PdfAnnotationSubtype } from '@embedpdf/models';
|
||||
|
||||
import { CustomSearchLayer } from './CustomSearchLayer';
|
||||
import { ZoomAPIBridge } from './ZoomAPIBridge';
|
||||
import ToolLoadingFallback from '../tools/ToolLoadingFallback';
|
||||
import { Center, Stack, Text } from '@mantine/core';
|
||||
import { ScrollAPIBridge } from './ScrollAPIBridge';
|
||||
import { SelectionAPIBridge } from './SelectionAPIBridge';
|
||||
import { PanAPIBridge } from './PanAPIBridge';
|
||||
import { SpreadAPIBridge } from './SpreadAPIBridge';
|
||||
import { SearchAPIBridge } from './SearchAPIBridge';
|
||||
import { ThumbnailAPIBridge } from './ThumbnailAPIBridge';
|
||||
import { RotateAPIBridge } from './RotateAPIBridge';
|
||||
|
||||
interface LocalEmbedPDFWithAnnotationsProps {
|
||||
file?: File | Blob;
|
||||
url?: string | null;
|
||||
onAnnotationChange?: (annotations: any[]) => void;
|
||||
}
|
||||
|
||||
export function LocalEmbedPDFWithAnnotations({
|
||||
file,
|
||||
url,
|
||||
onAnnotationChange
|
||||
}: LocalEmbedPDFWithAnnotationsProps) {
|
||||
const [pdfUrl, setPdfUrl] = useState<string | null>(null);
|
||||
|
||||
// Convert File to URL if needed
|
||||
useEffect(() => {
|
||||
if (file) {
|
||||
const objectUrl = URL.createObjectURL(file);
|
||||
setPdfUrl(objectUrl);
|
||||
return () => URL.revokeObjectURL(objectUrl);
|
||||
} else if (url) {
|
||||
setPdfUrl(url);
|
||||
}
|
||||
}, [file, url]);
|
||||
|
||||
// Create plugins configuration with annotation support
|
||||
const plugins = useMemo(() => {
|
||||
if (!pdfUrl) return [];
|
||||
|
||||
return [
|
||||
createPluginRegistration(LoaderPluginPackage, {
|
||||
loadingOptions: {
|
||||
type: 'url',
|
||||
pdfFile: {
|
||||
id: 'stirling-pdf-signing-viewer',
|
||||
url: pdfUrl,
|
||||
},
|
||||
},
|
||||
}),
|
||||
createPluginRegistration(ViewportPluginPackage, {
|
||||
viewportGap: 10,
|
||||
}),
|
||||
createPluginRegistration(ScrollPluginPackage, {
|
||||
strategy: ScrollStrategy.Vertical,
|
||||
initialPage: 0,
|
||||
}),
|
||||
createPluginRegistration(RenderPluginPackage),
|
||||
|
||||
// Register interaction manager (required for annotations)
|
||||
createPluginRegistration(InteractionManagerPluginPackage),
|
||||
|
||||
// Register selection plugin (depends on InteractionManager)
|
||||
createPluginRegistration(SelectionPluginPackage),
|
||||
|
||||
// Register history plugin for undo/redo (recommended for annotations)
|
||||
createPluginRegistration(HistoryPluginPackage),
|
||||
|
||||
// Register annotation plugin (depends on InteractionManager, Selection, History)
|
||||
createPluginRegistration(AnnotationPluginPackage, {
|
||||
annotationAuthor: 'Digital Signature',
|
||||
autoCommit: true,
|
||||
deactivateToolAfterCreate: false,
|
||||
selectAfterCreate: true,
|
||||
}),
|
||||
|
||||
// Register pan plugin
|
||||
createPluginRegistration(PanPluginPackage, {
|
||||
defaultMode: 'mobile',
|
||||
}),
|
||||
|
||||
// Register zoom plugin
|
||||
createPluginRegistration(ZoomPluginPackage, {
|
||||
defaultZoomLevel: 1.4,
|
||||
minZoom: 0.2,
|
||||
maxZoom: 3.0,
|
||||
}),
|
||||
|
||||
// Register tiling plugin
|
||||
createPluginRegistration(TilingPluginPackage, {
|
||||
tileSize: 768,
|
||||
overlapPx: 5,
|
||||
extraRings: 1,
|
||||
}),
|
||||
|
||||
// Register spread plugin
|
||||
createPluginRegistration(SpreadPluginPackage, {
|
||||
defaultSpreadMode: SpreadMode.None,
|
||||
}),
|
||||
|
||||
// Register search plugin
|
||||
createPluginRegistration(SearchPluginPackage),
|
||||
|
||||
// Register thumbnail plugin
|
||||
createPluginRegistration(ThumbnailPluginPackage),
|
||||
|
||||
// Register rotate plugin
|
||||
createPluginRegistration(RotatePluginPackage, {
|
||||
defaultRotation: Rotation.Degree0,
|
||||
}),
|
||||
];
|
||||
}, [pdfUrl]);
|
||||
|
||||
// Initialize the engine
|
||||
const { engine, isLoading, error } = usePdfiumEngine();
|
||||
|
||||
// Early return if no file or URL provided
|
||||
if (!file && !url) {
|
||||
return (
|
||||
<Center h="100%" w="100%">
|
||||
<Stack align="center" gap="md">
|
||||
<div style={{ fontSize: '24px' }}>📄</div>
|
||||
<Text c="dimmed" size="sm">
|
||||
No PDF provided
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || !engine || !pdfUrl) {
|
||||
return <ToolLoadingFallback toolName="PDF Engine" />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Center h="100%" w="100%">
|
||||
<Stack align="center" gap="md">
|
||||
<div style={{ fontSize: '24px' }}>❌</div>
|
||||
<Text c="red" size="sm" style={{ textAlign: 'center' }}>
|
||||
Error loading PDF engine: {error.message}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0
|
||||
}}>
|
||||
<EmbedPDF
|
||||
engine={engine}
|
||||
plugins={plugins}
|
||||
onInitialized={async (registry) => {
|
||||
const annotationPlugin = registry.getPlugin('annotation');
|
||||
if (!annotationPlugin || !annotationPlugin.provides) return;
|
||||
|
||||
const annotationApi = annotationPlugin.provides();
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Add custom signature stamp tool
|
||||
annotationApi.addTool({
|
||||
id: 'signatureStamp',
|
||||
name: 'Digital Signature',
|
||||
interaction: { exclusive: false, cursor: 'copy' },
|
||||
matchScore: () => 0,
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
// Will be set dynamically when user creates signature
|
||||
},
|
||||
});
|
||||
|
||||
// Add custom ink signature tool
|
||||
annotationApi.addTool({
|
||||
id: 'signatureInk',
|
||||
name: 'Signature Draw',
|
||||
interaction: { exclusive: true, cursor: 'crosshair' },
|
||||
matchScore: () => 0,
|
||||
defaults: {
|
||||
type: PdfAnnotationSubtype.INK,
|
||||
color: '#000000',
|
||||
opacity: 1.0,
|
||||
borderWidth: 2,
|
||||
},
|
||||
});
|
||||
|
||||
// Listen for annotation events to notify parent
|
||||
if (onAnnotationChange) {
|
||||
annotationApi.onAnnotationEvent((event: any) => {
|
||||
if (event.committed) {
|
||||
// Get all annotations and notify parent
|
||||
// This is a simplified approach - in reality you'd need to get all annotations
|
||||
onAnnotationChange([event.annotation]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ZoomAPIBridge />
|
||||
<ScrollAPIBridge />
|
||||
<SelectionAPIBridge />
|
||||
<PanAPIBridge />
|
||||
<SpreadAPIBridge />
|
||||
<SearchAPIBridge />
|
||||
<ThumbnailAPIBridge />
|
||||
<RotateAPIBridge />
|
||||
<GlobalPointerProvider>
|
||||
<Viewport
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-surface)',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
maxHeight: '100%',
|
||||
maxWidth: '100%',
|
||||
overflow: 'auto',
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
minWidth: 0,
|
||||
contain: 'strict',
|
||||
}}
|
||||
>
|
||||
<Scroller
|
||||
renderPage={({ width, height, pageIndex, scale, rotation }: {
|
||||
width: number;
|
||||
height: number;
|
||||
pageIndex: number;
|
||||
scale: number;
|
||||
rotation?: number;
|
||||
}) => (
|
||||
<Rotate pageSize={{ width, height }}>
|
||||
<PagePointerProvider {...{
|
||||
pageWidth: width,
|
||||
pageHeight: height,
|
||||
pageIndex,
|
||||
scale,
|
||||
rotation: rotation || 0
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
WebkitUserSelect: 'none',
|
||||
MozUserSelect: 'none',
|
||||
msUserSelect: 'none'
|
||||
}}
|
||||
draggable={false}
|
||||
onDragStart={(e) => e.preventDefault()}
|
||||
onDrop={(e) => e.preventDefault()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
{/* High-resolution tile layer */}
|
||||
<TilingLayer pageIndex={pageIndex} scale={scale} />
|
||||
|
||||
{/* Search highlight layer */}
|
||||
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
|
||||
|
||||
{/* Selection layer for text interaction */}
|
||||
<SelectionLayer pageIndex={pageIndex} scale={scale} />
|
||||
|
||||
{/* Annotation layer for signatures */}
|
||||
<AnnotationLayer
|
||||
pageIndex={pageIndex}
|
||||
scale={scale}
|
||||
pageWidth={width}
|
||||
pageHeight={height}
|
||||
rotation={rotation || 0}
|
||||
selectionOutlineColor="#007ACC"
|
||||
/>
|
||||
</div>
|
||||
</PagePointerProvider>
|
||||
</Rotate>
|
||||
)}
|
||||
/>
|
||||
</Viewport>
|
||||
</GlobalPointerProvider>
|
||||
</EmbedPDF>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button, Paper, Group, NumberInput } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
@@ -14,11 +14,11 @@ interface PdfViewerToolbarProps {
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
onPageChange?: (page: number) => void;
|
||||
|
||||
|
||||
// Dual page toggle (placeholder for now)
|
||||
dualPage?: boolean;
|
||||
onDualPageToggle?: () => void;
|
||||
|
||||
|
||||
// Zoom controls (connected via ViewerContext)
|
||||
currentZoom?: number;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export function PdfViewerToolbar({
|
||||
}: PdfViewerToolbarProps) {
|
||||
const { t } = useTranslation();
|
||||
const { getScrollState, getZoomState, scrollActions, zoomActions, registerImmediateZoomUpdate, registerImmediateScrollUpdate } = useViewer();
|
||||
|
||||
|
||||
const scrollState = getScrollState();
|
||||
const zoomState = getZoomState();
|
||||
const [pageInput, setPageInput] = useState(scrollState.currentPage || currentPage);
|
||||
@@ -151,7 +151,7 @@ export function PdfViewerToolbar({
|
||||
input: { width: 48, textAlign: "center", fontWeight: 500, fontSize: 16 },
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
<span style={{ fontWeight: 500, fontSize: 16 }}>
|
||||
/ {scrollState.totalPages}
|
||||
</span>
|
||||
@@ -229,4 +229,4 @@ export function PdfViewerToolbar({
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
import { useImperativeHandle, forwardRef, useEffect } from 'react';
|
||||
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
|
||||
import { PdfAnnotationSubtype, PdfStandardFont, PdfTextAlignment, PdfVerticalAlignment, uuidV4 } from '@embedpdf/models';
|
||||
import { SignParameters } from '../../hooks/tools/sign/useSignParameters';
|
||||
import { useSignature } from '../../contexts/SignatureContext';
|
||||
|
||||
export interface SignatureAPI {
|
||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => void;
|
||||
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => void;
|
||||
activateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
deleteAnnotation: (annotationId: string, pageIndex: number) => void;
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
deactivateTools: () => void;
|
||||
applySignatureFromParameters: (params: SignParameters) => void;
|
||||
getPageAnnotations: (pageIndex: number) => Promise<any[]>;
|
||||
}
|
||||
|
||||
export const SignatureAPIBridge = forwardRef<SignatureAPI>(function SignatureAPIBridge(_, ref) {
|
||||
const { provides: annotationApi } = useAnnotationCapability();
|
||||
const { signatureConfig, storeImageData, isPlacementMode } = useSignature();
|
||||
|
||||
|
||||
// Enable keyboard deletion of selected annotations - only when in signature placement mode
|
||||
useEffect(() => {
|
||||
if (!annotationApi || !isPlacementMode) return;
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
const selectedAnnotation = annotationApi.getSelectedAnnotation?.();
|
||||
|
||||
if (selectedAnnotation) {
|
||||
const annotation = selectedAnnotation as any;
|
||||
const pageIndex = annotation.object?.pageIndex || 0;
|
||||
const id = annotation.object?.id;
|
||||
|
||||
// For STAMP annotations, ensure image data is preserved before deletion
|
||||
if (annotation.object?.type === 13 && id) {
|
||||
// Get current annotation data to ensure we have latest image data stored
|
||||
const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex });
|
||||
if (pageAnnotationsTask) {
|
||||
pageAnnotationsTask.toPromise().then((pageAnnotations: any) => {
|
||||
const currentAnn = pageAnnotations?.find((ann: any) => ann.id === id);
|
||||
if (currentAnn && currentAnn.imageSrc) {
|
||||
// Ensure the image data is stored in our persistent store
|
||||
storeImageData(id, currentAnn.imageSrc);
|
||||
}
|
||||
}).catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Use EmbedPDF's native deletion which should integrate with history
|
||||
if ((annotationApi as any).deleteSelected) {
|
||||
(annotationApi as any).deleteSelected();
|
||||
} else {
|
||||
// Fallback to direct deletion - less ideal for history
|
||||
if (id) {
|
||||
annotationApi.deleteAnnotation(pageIndex, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [annotationApi, storeImageData, isPlacementMode]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
addImageSignature: (signatureData: string, x: number, y: number, width: number, height: number, pageIndex: number) => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Create image stamp annotation with proper image data
|
||||
|
||||
const annotationId = uuidV4();
|
||||
|
||||
// Store image data in our persistent store
|
||||
storeImageData(annotationId, signatureData);
|
||||
|
||||
annotationApi.createAnnotation(pageIndex, {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
rect: {
|
||||
origin: { x, y },
|
||||
size: { width, height }
|
||||
},
|
||||
author: 'Digital Signature',
|
||||
subject: 'Digital Signature',
|
||||
pageIndex: pageIndex,
|
||||
id: annotationId,
|
||||
created: new Date(),
|
||||
// Store image data in multiple places to ensure history captures it
|
||||
imageSrc: signatureData,
|
||||
contents: signatureData, // Some annotation systems use contents
|
||||
data: signatureData, // Try data field
|
||||
imageData: signatureData, // Try imageData field
|
||||
appearance: signatureData // Try appearance field
|
||||
});
|
||||
},
|
||||
|
||||
addTextSignature: (text: string, x: number, y: number, pageIndex: number) => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Create text annotation for signature
|
||||
annotationApi.createAnnotation(pageIndex, {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
rect: {
|
||||
origin: { x, y },
|
||||
size: { width: 200, height: 50 }
|
||||
},
|
||||
contents: text,
|
||||
author: 'Digital Signature',
|
||||
fontSize: 16,
|
||||
fontColor: '#000000',
|
||||
fontFamily: PdfStandardFont.Helvetica,
|
||||
textAlign: PdfTextAlignment.Left,
|
||||
verticalAlign: PdfVerticalAlignment.Top,
|
||||
opacity: 1,
|
||||
pageIndex: pageIndex,
|
||||
id: uuidV4(),
|
||||
created: new Date(),
|
||||
customData: {
|
||||
signatureText: text,
|
||||
signatureType: 'text'
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
activateDrawMode: () => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Activate the built-in ink tool for drawing
|
||||
annotationApi.setActiveTool('ink');
|
||||
|
||||
// Set default ink tool properties (black color, 2px width)
|
||||
const activeTool = annotationApi.getActiveTool();
|
||||
if (activeTool && activeTool.id === 'ink') {
|
||||
annotationApi.setToolDefaults('ink', {
|
||||
color: '#000000',
|
||||
thickness: 2,
|
||||
lineWidth: 2,
|
||||
strokeWidth: 2,
|
||||
width: 2
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
activateSignaturePlacementMode: () => {
|
||||
if (!annotationApi || !signatureConfig) return;
|
||||
|
||||
try {
|
||||
if (signatureConfig.signatureType === 'text' && signatureConfig.signerName) {
|
||||
// Try different tool names for text annotations
|
||||
const textToolNames = ['freetext', 'text', 'textbox', 'annotation-text'];
|
||||
let activatedTool = null;
|
||||
|
||||
for (const toolName of textToolNames) {
|
||||
annotationApi.setActiveTool(toolName);
|
||||
const tool = annotationApi.getActiveTool();
|
||||
|
||||
if (tool && tool.id === toolName) {
|
||||
activatedTool = tool;
|
||||
annotationApi.setToolDefaults(toolName, {
|
||||
contents: signatureConfig.signerName,
|
||||
fontSize: signatureConfig.fontSize || 16,
|
||||
fontFamily: signatureConfig.fontFamily === 'Times-Roman' ? PdfStandardFont.Times_Roman :
|
||||
signatureConfig.fontFamily === 'Courier' ? PdfStandardFont.Courier :
|
||||
PdfStandardFont.Helvetica,
|
||||
fontColor: '#000000',
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!activatedTool) {
|
||||
// Fallback: create a simple text image as stamp
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
const fontSize = signatureConfig.fontSize || 16;
|
||||
const fontFamily = signatureConfig.fontFamily || 'Helvetica';
|
||||
|
||||
canvas.width = Math.max(200, signatureConfig.signerName.length * fontSize * 0.6);
|
||||
canvas.height = fontSize + 20;
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.font = `${fontSize}px ${fontFamily}`;
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(signatureConfig.signerName, 10, canvas.height / 2);
|
||||
const dataURL = canvas.toDataURL();
|
||||
|
||||
annotationApi.setActiveTool('stamp');
|
||||
const stampTool = annotationApi.getActiveTool();
|
||||
if (stampTool && stampTool.id === 'stamp') {
|
||||
annotationApi.setToolDefaults('stamp', {
|
||||
imageSrc: dataURL,
|
||||
subject: `Text Signature - ${signatureConfig.signerName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (signatureConfig.signatureData) {
|
||||
// Use stamp tool for image/canvas signatures
|
||||
annotationApi.setActiveTool('stamp');
|
||||
const activeTool = annotationApi.getActiveTool();
|
||||
if (activeTool && activeTool.id === 'stamp') {
|
||||
annotationApi.setToolDefaults('stamp', {
|
||||
imageSrc: signatureConfig.signatureData,
|
||||
subject: `Digital Signature - ${signatureConfig.reason || 'Document signing'}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error activating signature tool:', error);
|
||||
}
|
||||
},
|
||||
|
||||
updateDrawSettings: (color: string, size: number) => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Always update ink tool defaults - use multiple property names for compatibility
|
||||
annotationApi.setToolDefaults('ink', {
|
||||
color: color,
|
||||
thickness: size,
|
||||
lineWidth: size,
|
||||
strokeWidth: size,
|
||||
width: size
|
||||
});
|
||||
|
||||
// Force reactivate ink tool to ensure new settings take effect
|
||||
annotationApi.setActiveTool(null); // Deactivate first
|
||||
setTimeout(() => {
|
||||
annotationApi.setActiveTool('ink'); // Reactivate with new settings
|
||||
}, 50);
|
||||
},
|
||||
|
||||
activateDeleteMode: () => {
|
||||
if (!annotationApi) return;
|
||||
// Activate selection tool to allow selecting and deleting annotations
|
||||
// Users can click annotations to select them, then press Delete key or right-click to delete
|
||||
annotationApi.setActiveTool('select');
|
||||
},
|
||||
|
||||
deleteAnnotation: (annotationId: string, pageIndex: number) => {
|
||||
if (!annotationApi) return;
|
||||
|
||||
// Before deleting, try to preserve image data for potential undo
|
||||
const pageAnnotationsTask = annotationApi.getPageAnnotations?.({ pageIndex });
|
||||
if (pageAnnotationsTask) {
|
||||
pageAnnotationsTask.toPromise().then((pageAnnotations: any) => {
|
||||
const annotation = pageAnnotations?.find((ann: any) => ann.id === annotationId);
|
||||
if (annotation && annotation.type === 13 && annotation.imageSrc) {
|
||||
// Store image data before deletion
|
||||
storeImageData(annotationId, annotation.imageSrc);
|
||||
}
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
// Delete specific annotation by ID
|
||||
annotationApi.deleteAnnotation(pageIndex, annotationId);
|
||||
},
|
||||
|
||||
deactivateTools: () => {
|
||||
if (!annotationApi) return;
|
||||
annotationApi.setActiveTool(null);
|
||||
},
|
||||
|
||||
applySignatureFromParameters: (params: SignParameters) => {
|
||||
if (!annotationApi || !params.signaturePosition) return;
|
||||
|
||||
const { x, y, width, height, page } = params.signaturePosition;
|
||||
|
||||
switch (params.signatureType) {
|
||||
case 'image':
|
||||
if (params.signatureData) {
|
||||
const annotationId = uuidV4();
|
||||
|
||||
// Store image data in our persistent store
|
||||
storeImageData(annotationId, params.signatureData);
|
||||
|
||||
annotationApi.createAnnotation(page, {
|
||||
type: PdfAnnotationSubtype.STAMP,
|
||||
rect: {
|
||||
origin: { x, y },
|
||||
size: { width, height }
|
||||
},
|
||||
author: 'Digital Signature',
|
||||
subject: `Digital Signature - ${params.reason || 'Document signing'}`,
|
||||
pageIndex: page,
|
||||
id: annotationId,
|
||||
created: new Date(),
|
||||
// Store image data in multiple places to ensure history captures it
|
||||
imageSrc: params.signatureData,
|
||||
contents: params.signatureData, // Some annotation systems use contents
|
||||
data: params.signatureData, // Try data field
|
||||
imageData: params.signatureData, // Try imageData field
|
||||
appearance: params.signatureData // Try appearance field
|
||||
});
|
||||
|
||||
// Switch to select mode after placing signature so it can be easily deleted
|
||||
setTimeout(() => {
|
||||
annotationApi.setActiveTool('select');
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
if (params.signerName) {
|
||||
annotationApi.createAnnotation(page, {
|
||||
type: PdfAnnotationSubtype.FREETEXT,
|
||||
rect: {
|
||||
origin: { x, y },
|
||||
size: { width, height }
|
||||
},
|
||||
contents: params.signerName,
|
||||
author: 'Digital Signature',
|
||||
fontSize: 16,
|
||||
fontColor: '#000000',
|
||||
fontFamily: PdfStandardFont.Helvetica,
|
||||
textAlign: PdfTextAlignment.Left,
|
||||
verticalAlign: PdfVerticalAlignment.Top,
|
||||
opacity: 1,
|
||||
pageIndex: page,
|
||||
id: uuidV4(),
|
||||
created: new Date(),
|
||||
customData: {
|
||||
signatureText: params.signerName,
|
||||
signatureType: 'text'
|
||||
}
|
||||
});
|
||||
|
||||
// Switch to select mode after placing signature so it can be easily deleted
|
||||
setTimeout(() => {
|
||||
annotationApi.setActiveTool('select');
|
||||
}, 100);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'draw':
|
||||
// For draw mode, we activate the tool and let user draw
|
||||
annotationApi.setActiveTool('ink');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
getPageAnnotations: async (pageIndex: number): Promise<any[]> => {
|
||||
if (!annotationApi || !annotationApi.getPageAnnotations) {
|
||||
console.warn('getPageAnnotations not available');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const pageAnnotationsTask = annotationApi.getPageAnnotations({ pageIndex });
|
||||
if (pageAnnotationsTask && pageAnnotationsTask.toPromise) {
|
||||
const annotations = await pageAnnotationsTask.toPromise();
|
||||
return annotations || [];
|
||||
}
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error(`Error getting annotations for page ${pageIndex}:`, error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
}), [annotationApi, signatureConfig]);
|
||||
|
||||
|
||||
return null; // This is a bridge component with no UI
|
||||
});
|
||||
|
||||
SignatureAPIBridge.displayName = 'SignatureAPIBridge';
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Box, ScrollArea } from '@mantine/core';
|
||||
import { useViewer } from '../../contexts/ViewerContext';
|
||||
|
||||
@@ -25,7 +25,7 @@ export function ThumbnailSidebar({ visible, onToggle: _onToggle }: ThumbnailSide
|
||||
});
|
||||
setThumbnails({});
|
||||
}
|
||||
}, [visible, thumbnails]);
|
||||
}, [visible]); // Remove thumbnails from dependency to prevent infinite loop
|
||||
|
||||
// Generate thumbnails when sidebar becomes visible
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import EmbedPdfViewer from './EmbedPdfViewer';
|
||||
|
||||
export interface ViewerProps {
|
||||
@@ -13,4 +12,4 @@ const Viewer = (props: ViewerProps) => {
|
||||
return <EmbedPdfViewer {...props} />;
|
||||
};
|
||||
|
||||
export default Viewer;
|
||||
export default Viewer;
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ENDPOINTS = {
|
||||
export type SplitMethod = typeof SPLIT_METHODS[keyof typeof SPLIT_METHODS];
|
||||
export const isSplitMethod = (value: string | null): value is SplitMethod => {
|
||||
return Object.values(SPLIT_METHODS).includes(value as SplitMethod);
|
||||
}
|
||||
};
|
||||
|
||||
import { CardOption } from '../components/shared/CardSelector';
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
|
||||
*/
|
||||
|
||||
import React, { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import {
|
||||
FileContextProviderProps,
|
||||
FileContextSelectors,
|
||||
@@ -76,7 +76,7 @@ function FileContextInner({
|
||||
const currentSelection = stateRef.current.ui.selectedFileIds;
|
||||
const newFileIds = stirlingFiles.map(stirlingFile => stirlingFile.fileId);
|
||||
dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } });
|
||||
}
|
||||
};
|
||||
|
||||
// File operations using unified addFiles helper with persistence
|
||||
const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
|
||||
|
||||
@@ -109,16 +109,34 @@ export const NavigationProvider: React.FC<{
|
||||
|
||||
const actions: NavigationContextActions = {
|
||||
setWorkbench: useCallback((workbench: WorkbenchType) => {
|
||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||
}, []),
|
||||
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
||||
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
||||
const performWorkbenchChange = () => {
|
||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||
};
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||
} else {
|
||||
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
|
||||
}
|
||||
}, [state.workbench, state.hasUnsavedChanges]),
|
||||
|
||||
setSelectedTool: useCallback((toolId: ToolId | null) => {
|
||||
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
|
||||
}, []),
|
||||
|
||||
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
|
||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||
}, []),
|
||||
// If we're leaving pageEditor workbench and have unsaved changes, request navigation
|
||||
if (state.workbench === 'pageEditor' && workbench !== 'pageEditor' && state.hasUnsavedChanges) {
|
||||
const performWorkbenchChange = () => {
|
||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||
};
|
||||
dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: performWorkbenchChange } });
|
||||
dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } });
|
||||
} else {
|
||||
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
|
||||
}
|
||||
}, [state.workbench, state.hasUnsavedChanges]),
|
||||
|
||||
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
|
||||
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useState, useRef, useMemo } from 'react';
|
||||
import { createContext, useContext, useState, useRef, useMemo } from 'react';
|
||||
import { SidebarState, SidebarRefs, SidebarContextValue, SidebarProviderProps } from '../types/sidebar';
|
||||
|
||||
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
|
||||
@@ -7,7 +7,7 @@ export function SidebarProvider({ children }: SidebarProviderProps) {
|
||||
// All sidebar state management
|
||||
const quickAccessRef = useRef<HTMLDivElement>(null);
|
||||
const toolPanelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const [sidebarsVisible, setSidebarsVisible] = useState(true);
|
||||
const [leftPanelView, setLeftPanelView] = useState<'toolPicker' | 'toolContent'>('toolPicker');
|
||||
const [readerMode, setReaderMode] = useState(false);
|
||||
@@ -44,4 +44,4 @@ export function useSidebarContext(): SidebarContextValue {
|
||||
throw new Error('useSidebarContext must be used within a SidebarProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useCallback, useRef } from 'react';
|
||||
import { SignParameters } from '../hooks/tools/sign/useSignParameters';
|
||||
import { SignatureAPI } from '../components/viewer/SignatureAPIBridge';
|
||||
import { HistoryAPI } from '../components/viewer/HistoryAPIBridge';
|
||||
|
||||
// Signature state interface
|
||||
interface SignatureState {
|
||||
// Current signature configuration from the tool
|
||||
signatureConfig: SignParameters | null;
|
||||
// Whether we're in signature placement mode
|
||||
isPlacementMode: boolean;
|
||||
// Whether signatures have been applied (allows export)
|
||||
signaturesApplied: boolean;
|
||||
}
|
||||
|
||||
// Signature actions interface
|
||||
interface SignatureActions {
|
||||
setSignatureConfig: (config: SignParameters | null) => void;
|
||||
setPlacementMode: (enabled: boolean) => void;
|
||||
activateDrawMode: () => void;
|
||||
deactivateDrawMode: () => void;
|
||||
activateSignaturePlacementMode: () => void;
|
||||
activateDeleteMode: () => void;
|
||||
updateDrawSettings: (color: string, size: number) => void;
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
storeImageData: (id: string, data: string) => void;
|
||||
getImageData: (id: string) => string | undefined;
|
||||
setSignaturesApplied: (applied: boolean) => void;
|
||||
}
|
||||
|
||||
// Combined context interface
|
||||
interface SignatureContextValue extends SignatureState, SignatureActions {
|
||||
signatureApiRef: React.RefObject<SignatureAPI | null>;
|
||||
historyApiRef: React.RefObject<HistoryAPI | null>;
|
||||
}
|
||||
|
||||
// Create context
|
||||
const SignatureContext = createContext<SignatureContextValue | undefined>(undefined);
|
||||
|
||||
// Initial state
|
||||
const initialState: SignatureState = {
|
||||
signatureConfig: null,
|
||||
isPlacementMode: false,
|
||||
signaturesApplied: true, // Start as true (no signatures placed yet)
|
||||
};
|
||||
|
||||
// Provider component
|
||||
export const SignatureProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [state, setState] = useState<SignatureState>(initialState);
|
||||
const signatureApiRef = useRef<SignatureAPI>(null);
|
||||
const historyApiRef = useRef<HistoryAPI>(null);
|
||||
const imageDataStore = useRef<Map<string, string>>(new Map());
|
||||
|
||||
// Actions
|
||||
const setSignatureConfig = useCallback((config: SignParameters | null) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
signatureConfig: config,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const setPlacementMode = useCallback((enabled: boolean) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
isPlacementMode: enabled,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const activateDrawMode = useCallback(() => {
|
||||
if (signatureApiRef.current) {
|
||||
signatureApiRef.current.activateDrawMode();
|
||||
setPlacementMode(true);
|
||||
// Mark signatures as not applied when entering draw mode
|
||||
setState(prev => ({ ...prev, signaturesApplied: false }));
|
||||
}
|
||||
}, [setPlacementMode]);
|
||||
|
||||
const deactivateDrawMode = useCallback(() => {
|
||||
if (signatureApiRef.current) {
|
||||
signatureApiRef.current.deactivateTools();
|
||||
setPlacementMode(false);
|
||||
}
|
||||
}, [setPlacementMode]);
|
||||
|
||||
const activateSignaturePlacementMode = useCallback(() => {
|
||||
if (signatureApiRef.current) {
|
||||
signatureApiRef.current.activateSignaturePlacementMode();
|
||||
setPlacementMode(true);
|
||||
// Mark signatures as not applied when placing new signatures
|
||||
setState(prev => ({ ...prev, signaturesApplied: false }));
|
||||
}
|
||||
}, [setPlacementMode]);
|
||||
|
||||
const activateDeleteMode = useCallback(() => {
|
||||
if (signatureApiRef.current) {
|
||||
signatureApiRef.current.activateDeleteMode();
|
||||
setPlacementMode(true);
|
||||
}
|
||||
}, [setPlacementMode]);
|
||||
|
||||
const updateDrawSettings = useCallback((color: string, size: number) => {
|
||||
if (signatureApiRef.current) {
|
||||
signatureApiRef.current.updateDrawSettings(color, size);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
if (historyApiRef.current) {
|
||||
historyApiRef.current.undo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const redo = useCallback(() => {
|
||||
if (historyApiRef.current) {
|
||||
historyApiRef.current.redo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const storeImageData = useCallback((id: string, data: string) => {
|
||||
imageDataStore.current.set(id, data);
|
||||
}, []);
|
||||
|
||||
const getImageData = useCallback((id: string) => {
|
||||
return imageDataStore.current.get(id);
|
||||
}, []);
|
||||
|
||||
const setSignaturesApplied = useCallback((applied: boolean) => {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
signaturesApplied: applied,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// No auto-activation - all modes use manual buttons
|
||||
|
||||
const contextValue: SignatureContextValue = {
|
||||
...state,
|
||||
signatureApiRef,
|
||||
historyApiRef,
|
||||
setSignatureConfig,
|
||||
setPlacementMode,
|
||||
activateDrawMode,
|
||||
deactivateDrawMode,
|
||||
activateSignaturePlacementMode,
|
||||
activateDeleteMode,
|
||||
updateDrawSettings,
|
||||
undo,
|
||||
redo,
|
||||
storeImageData,
|
||||
getImageData,
|
||||
setSignaturesApplied,
|
||||
};
|
||||
|
||||
return (
|
||||
<SignatureContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</SignatureContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use signature context
|
||||
export const useSignature = (): SignatureContextValue => {
|
||||
const context = useContext(SignatureContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useSignature must be used within a SignatureProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
// Hook for components that need to check if signature mode is active
|
||||
export const useSignatureMode = () => {
|
||||
const context = useContext(SignatureContext);
|
||||
return {
|
||||
isSignatureModeActive: context?.isPlacementMode || false,
|
||||
hasSignatureConfig: context?.signatureConfig !== null,
|
||||
};
|
||||
};
|
||||
@@ -17,7 +17,7 @@ import { filterToolRegistryByQuery } from '../utils/toolSearch';
|
||||
interface ToolWorkflowState {
|
||||
// UI State
|
||||
sidebarsVisible: boolean;
|
||||
leftPanelView: 'toolPicker' | 'toolContent';
|
||||
leftPanelView: 'toolPicker' | 'toolContent' | 'hidden';
|
||||
readerMode: boolean;
|
||||
|
||||
// File/Preview State
|
||||
@@ -31,7 +31,7 @@ interface ToolWorkflowState {
|
||||
// Actions
|
||||
type ToolWorkflowAction =
|
||||
| { type: 'SET_SIDEBARS_VISIBLE'; payload: boolean }
|
||||
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' }
|
||||
| { type: 'SET_LEFT_PANEL_VIEW'; payload: 'toolPicker' | 'toolContent' | 'hidden' }
|
||||
| { type: 'SET_READER_MODE'; payload: boolean }
|
||||
| { type: 'SET_PREVIEW_FILE'; payload: File | null }
|
||||
| { type: 'SET_PAGE_EDITOR_FUNCTIONS'; payload: PageEditorFunctions | null }
|
||||
@@ -80,7 +80,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
|
||||
// UI Actions
|
||||
setSidebarsVisible: (visible: boolean) => void;
|
||||
setLeftPanelView: (view: 'toolPicker' | 'toolContent') => void;
|
||||
setLeftPanelView: (view: 'toolPicker' | 'toolContent' | 'hidden') => void;
|
||||
setReaderMode: (mode: boolean) => void;
|
||||
setPreviewFile: (file: File | null) => void;
|
||||
setPageEditorFunctions: (functions: PageEditorFunctions | null) => void;
|
||||
@@ -96,7 +96,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState {
|
||||
resetTool: (toolId: string) => void;
|
||||
|
||||
// Workflow Actions (compound actions)
|
||||
handleToolSelect: (toolId: string) => void;
|
||||
handleToolSelect: (toolId: ToolId) => void;
|
||||
handleBackToTools: () => void;
|
||||
handleReaderToggle: () => void;
|
||||
|
||||
@@ -136,7 +136,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
dispatch({ type: 'SET_SIDEBARS_VISIBLE', payload: visible });
|
||||
}, []);
|
||||
|
||||
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent') => {
|
||||
const setLeftPanelView = useCallback((view: 'toolPicker' | 'toolContent' | 'hidden') => {
|
||||
dispatch({ type: 'SET_LEFT_PANEL_VIEW', payload: view });
|
||||
}, []);
|
||||
|
||||
@@ -180,7 +180,26 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
}, []); // Empty dependency array makes this stable
|
||||
|
||||
// Workflow actions (compound actions that coordinate multiple state changes)
|
||||
const handleToolSelect = useCallback((toolId: string) => {
|
||||
const handleToolSelect = useCallback((toolId: ToolId) => {
|
||||
// Handle read tool selection - should behave exactly like QuickAccessBar read button
|
||||
if (toolId === 'read') {
|
||||
setReaderMode(true);
|
||||
actions.setSelectedTool('read');
|
||||
actions.setWorkbench('viewer');
|
||||
setSearchQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle multiTool selection - enable page editor workbench and hide left panel
|
||||
if (toolId === 'multiTool') {
|
||||
setReaderMode(false);
|
||||
setLeftPanelView('hidden');
|
||||
actions.setSelectedTool('multiTool');
|
||||
actions.setWorkbench('pageEditor');
|
||||
setSearchQuery('');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the selected tool and determine the appropriate workbench
|
||||
const validToolId = isValidToolId(toolId) ? toolId : null;
|
||||
actions.setSelectedTool(validToolId);
|
||||
@@ -195,19 +214,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
|
||||
// Clear search query when selecting a tool
|
||||
setSearchQuery('');
|
||||
|
||||
// Handle view switching logic
|
||||
if (toolId === 'allTools' || toolId === 'read' || toolId === 'view-pdf') {
|
||||
setLeftPanelView('toolPicker');
|
||||
if (toolId === 'read' || toolId === 'view-pdf') {
|
||||
setReaderMode(true);
|
||||
} else {
|
||||
setReaderMode(false);
|
||||
}
|
||||
} else {
|
||||
setLeftPanelView('toolContent');
|
||||
setReaderMode(false); // Disable read mode when selecting tools
|
||||
}
|
||||
setLeftPanelView('toolContent');
|
||||
setReaderMode(false); // Disable read mode when selecting tools
|
||||
}, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]);
|
||||
|
||||
const handleBackToTools = useCallback(() => {
|
||||
@@ -227,8 +235,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) {
|
||||
}, [toolRegistry, state.searchQuery]);
|
||||
|
||||
const isPanelVisible = useMemo(() =>
|
||||
state.sidebarsVisible && !state.readerMode,
|
||||
[state.sidebarsVisible, state.readerMode]
|
||||
state.sidebarsVisible && !state.readerMode && state.leftPanelView !== 'hidden',
|
||||
[state.sidebarsVisible, state.readerMode, state.leftPanelView]
|
||||
);
|
||||
|
||||
// URL sync for proper tool navigation
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { createContext, useContext, useState, ReactNode, useRef } from 'react';
|
||||
import { SpreadMode } from '@embedpdf/plugin-spread/react';
|
||||
import { useNavigation } from './NavigationContext';
|
||||
|
||||
// Bridge API interfaces - these match what the bridges provide
|
||||
interface ScrollAPIWrapper {
|
||||
@@ -208,6 +209,9 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
|
||||
// UI state - only state directly managed by this context
|
||||
const [isThumbnailSidebarVisible, setIsThumbnailSidebarVisible] = useState(false);
|
||||
|
||||
// Get current navigation state to check if we're in sign mode
|
||||
useNavigation();
|
||||
|
||||
// Bridge registry - bridges register their state and APIs here
|
||||
const bridgeRefs = useRef({
|
||||
scroll: null as BridgeRef<ScrollState, ScrollAPIWrapper> | null,
|
||||
|
||||
@@ -22,6 +22,7 @@ import Merge from '../tools/Merge';
|
||||
import Repair from "../tools/Repair";
|
||||
import AutoRename from "../tools/AutoRename";
|
||||
import SingleLargePage from "../tools/SingleLargePage";
|
||||
import PageLayout from "../tools/PageLayout";
|
||||
import UnlockPdfForms from "../tools/UnlockPdfForms";
|
||||
import RemoveCertificateSign from "../tools/RemoveCertificateSign";
|
||||
import RemoveImage from "../tools/RemoveImage";
|
||||
@@ -31,6 +32,7 @@ import Flatten from "../tools/Flatten";
|
||||
import Rotate from "../tools/Rotate";
|
||||
import ChangeMetadata from "../tools/ChangeMetadata";
|
||||
import Crop from "../tools/Crop";
|
||||
import Sign from "../tools/Sign";
|
||||
import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation";
|
||||
import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation";
|
||||
import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation";
|
||||
@@ -54,7 +56,9 @@ import { flattenOperationConfig } from "../hooks/tools/flatten/useFlattenOperati
|
||||
import { redactOperationConfig } from "../hooks/tools/redact/useRedactOperation";
|
||||
import { rotateOperationConfig } from "../hooks/tools/rotate/useRotateOperation";
|
||||
import { changeMetadataOperationConfig } from "../hooks/tools/changeMetadata/useChangeMetadataOperation";
|
||||
import { signOperationConfig } from "../hooks/tools/sign/useSignOperation";
|
||||
import { cropOperationConfig } from "../hooks/tools/crop/useCropOperation";
|
||||
import { removeAnnotationsOperationConfig } from "../hooks/tools/removeAnnotations/useRemoveAnnotationsOperation";
|
||||
import { extractImagesOperationConfig } from "../hooks/tools/extractImages/useExtractImagesOperation";
|
||||
import { replaceColorOperationConfig } from "../hooks/tools/replaceColor/useReplaceColorOperation";
|
||||
import CompressSettings from "../components/tools/compress/CompressSettings";
|
||||
@@ -84,7 +88,13 @@ import { scannerImageSplitOperationConfig } from "../hooks/tools/scannerImageSpl
|
||||
import AdjustPageScaleSettings from "../components/tools/adjustPageScale/AdjustPageScaleSettings";
|
||||
import ScannerImageSplitSettings from "../components/tools/scannerImageSplit/ScannerImageSplitSettings";
|
||||
import ChangeMetadataSingleStep from "../components/tools/changeMetadata/ChangeMetadataSingleStep";
|
||||
import SignSettings from "../components/tools/sign/SignSettings";
|
||||
import CropSettings from "../components/tools/crop/CropSettings";
|
||||
import AddPageNumbers from "../tools/AddPageNumbers";
|
||||
import { addPageNumbersOperationConfig } from "../components/tools/addPageNumbers/useAddPageNumbersOperation";
|
||||
import RemoveAnnotations from "../tools/RemoveAnnotations";
|
||||
import RemoveAnnotationsSettings from "../components/tools/removeAnnotations/RemoveAnnotationsSettings";
|
||||
import PageLayoutSettings from "../components/tools/pageLayout/PageLayoutSettings";
|
||||
import ExtractImages from "../tools/ExtractImages";
|
||||
import ExtractImagesSettings from "../components/tools/extractImages/ExtractImagesSettings";
|
||||
import ReplaceColorSettings from "../components/tools/replaceColor/ReplaceColorSettings";
|
||||
@@ -178,8 +188,32 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
|
||||
return useMemo(() => {
|
||||
const allTools: ToolRegistry = {
|
||||
// Recommended Tools in order
|
||||
multiTool: {
|
||||
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.multiTool.title", "Multi-Tool"),
|
||||
component: null,
|
||||
workbench: "pageEditor",
|
||||
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
|
||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
maxFiles: -1,
|
||||
synonyms: getSynonyms(t, "multiTool"),
|
||||
},
|
||||
merge: {
|
||||
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.merge.title", "Merge"),
|
||||
component: Merge,
|
||||
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
|
||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
maxFiles: -1,
|
||||
endpoints: ["merge-pdfs"],
|
||||
operationConfig: mergeOperationConfig,
|
||||
settingsComponent: MergeSettings,
|
||||
synonyms: getSynonyms(t, "merge")
|
||||
},
|
||||
// Signing
|
||||
|
||||
certSign: {
|
||||
icon: <LocalIcon icon="workspace-premium-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.certSign.title", "Certificate Sign"),
|
||||
@@ -196,10 +230,12 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
sign: {
|
||||
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.sign.title", "Sign"),
|
||||
component: null,
|
||||
component: Sign,
|
||||
description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.SIGNING,
|
||||
operationConfig: signOperationConfig,
|
||||
settingsComponent: SignSettings,
|
||||
synonyms: getSynonyms(t, "sign")
|
||||
},
|
||||
|
||||
@@ -410,21 +446,25 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
addPageNumbers: {
|
||||
icon: <LocalIcon icon="123-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.addPageNumbers.title", "Add Page Numbers"),
|
||||
component: null,
|
||||
|
||||
component: AddPageNumbers,
|
||||
description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["add-page-numbers"],
|
||||
operationConfig: addPageNumbersOperationConfig,
|
||||
synonyms: getSynonyms(t, "addPageNumbers")
|
||||
},
|
||||
pageLayout: {
|
||||
icon: <LocalIcon icon="dashboard-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.pageLayout.title", "Multi-Page Layout"),
|
||||
component: null,
|
||||
|
||||
component: PageLayout,
|
||||
description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.PAGE_FORMATTING,
|
||||
maxFiles: -1,
|
||||
endpoints: ["multi-page-layout"],
|
||||
settingsComponent: PageLayoutSettings,
|
||||
synonyms: getSynonyms(t, "pageLayout")
|
||||
},
|
||||
bookletImposition: {
|
||||
@@ -517,10 +557,13 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
removeAnnotations: {
|
||||
icon: <LocalIcon icon="thread-unread-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.removeAnnotations.title", "Remove Annotations"),
|
||||
component: null,
|
||||
component: RemoveAnnotations,
|
||||
description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"),
|
||||
categoryId: ToolCategoryId.STANDARD_TOOLS,
|
||||
subcategoryId: SubcategoryId.REMOVAL,
|
||||
maxFiles: -1,
|
||||
operationConfig: removeAnnotationsOperationConfig,
|
||||
settingsComponent: RemoveAnnotationsSettings,
|
||||
synonyms: getSynonyms(t, "removeAnnotations")
|
||||
},
|
||||
removeImage: {
|
||||
@@ -782,30 +825,7 @@ export function useFlatToolRegistry(): ToolRegistry {
|
||||
settingsComponent: ConvertSettings,
|
||||
synonyms: getSynonyms(t, "convert")
|
||||
},
|
||||
merge: {
|
||||
icon: <LocalIcon icon="library-add-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.merge.title", "Merge"),
|
||||
component: Merge,
|
||||
description: t("home.merge.desc", "Merge multiple PDFs into a single document"),
|
||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
maxFiles: -1,
|
||||
endpoints: ["merge-pdfs"],
|
||||
operationConfig: mergeOperationConfig,
|
||||
settingsComponent: MergeSettings,
|
||||
synonyms: getSynonyms(t, "merge")
|
||||
},
|
||||
multiTool: {
|
||||
icon: <LocalIcon icon="dashboard-customize-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.multiTool.title", "Multi-Tool"),
|
||||
component: null,
|
||||
workbench: "pageEditor",
|
||||
description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"),
|
||||
categoryId: ToolCategoryId.RECOMMENDED_TOOLS,
|
||||
subcategoryId: SubcategoryId.GENERAL,
|
||||
maxFiles: -1,
|
||||
synonyms: getSynonyms(t, "multiTool"),
|
||||
},
|
||||
|
||||
ocr: {
|
||||
icon: <LocalIcon icon="quick-reference-all-outline-rounded" width="1.5rem" height="1.5rem" />,
|
||||
name: t("home.ocr.title", "OCR"),
|
||||
|
||||
Vendored
-1
@@ -18,6 +18,5 @@ declare module '../assets/material-symbols-icons.json' {
|
||||
}
|
||||
|
||||
declare module 'pdfjs-dist/legacy/build/pdf.mjs'
|
||||
// TODO: Add proper EmbedPDF types for local submodule integration
|
||||
|
||||
export {};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../../services/apiClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ConvertParameters, defaultParameters } from './useConvertParameters';
|
||||
import { createFileFromApiResponse } from '../../../utils/fileResponseUtils';
|
||||
@@ -108,7 +108,7 @@ export const convertProcessor = async (
|
||||
for (const file of selectedFiles) {
|
||||
try {
|
||||
const formData = buildConvertFormData(parameters, [file]);
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
const response = await apiClient.post(endpoint, formData, { responseType: 'blob' });
|
||||
|
||||
const convertedFile = createFileFromResponse(response.data, response.headers, file.name, parameters.toExtension);
|
||||
|
||||
@@ -120,7 +120,7 @@ export const convertProcessor = async (
|
||||
} else {
|
||||
// Batch processing for simple cases (image→PDF combine)
|
||||
const formData = buildConvertFormData(parameters, selectedFiles);
|
||||
const response = await axios.post(endpoint, formData, { responseType: 'blob' });
|
||||
const response = await apiClient.post(endpoint, formData, { responseType: 'blob' });
|
||||
|
||||
const baseFilename = selectedFiles.length === 1
|
||||
? selectedFiles[0].name
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user