Compare commits

..
Author SHA1 Message Date
Anthony StirlingandClaude 2ef63566f1 Fix default parameter consistency and cleanup formatting
- Update tolerance default from 20 to 30 to match documentation
- Update min_area default from 8000 to 10000 to match documentation
- Add missing period to minimum contour area description
- Replace double dashes with single hyphens in tooltip translations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 00:05:55 +01:00
Anthony StirlingandClaude 00efc8802c Fix TypeScript linting error in zipFileService
Remove unused filename parameter from filter function to resolve
@typescript-eslint/no-unused-vars error. Also include spotless
formatting changes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-26 00:01:21 +01:00
Anthony StirlingandClaude 9973c6b1d3 Rewrite tooltip with concise, actionable content focused on real-world usage
Transform verbose tooltip into practical guidance:

New structure:
• "What this does" - Clear one-liner about functionality
• "When to use" - 4 concrete use cases (album pages, batches, collages, documents)
• "Quick fixes" - Problem → solution pairs with specific values
• "Setup tips" - Practical scanning advice (DPI, gaps, backgrounds)
• "Heads-up" - Important limitations and workarounds

Benefits:
- Shorter, scannable content
- Action-oriented language ("increase Tolerance to 30-50")
- Real-world scenarios users actually face
- Specific numeric guidance instead of vague descriptions
- Proactive troubleshooting before problems occur

Content matches professional but accessible tone throughout.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 23:55:57 +01:00
Anthony StirlingandClaude 5d828040a7 Replace abstract tooltip explanations with practical troubleshooting guide
- Remove unhelpful technical descriptions with meaningless numbers
- Add problem-solution pairs with specific value recommendations:
  • Photos not detected → increase Tolerance to 30-50
  • Too many false detections → increase Minimum Area to 15000-20000
  • Photos cropped tight → increase Border Size to 5-10
  • Tilted photos → lower Angle Threshold to 5
  • Dust detection → increase Minimum Contour Area to 1000

- Improve setup tips with concrete measurements:
  • Use white/light backgrounds
  • Leave 1cm gaps between photos
  • Scan at 300+ DPI
  • Try defaults before adjusting

Now users get actionable solutions instead of abstract technical jargon.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 23:44:20 +01:00
Anthony StirlingandClaude d6a3774e9b Add comprehensive tooltip support for scanner image split tool
- Create useScannerImageSplitTips with detailed explanations
- Explain use cases: photo album digitisation, flatbed scanning, collage splitting
- Add technical explanations for all 5 parameters with user-friendly language
- Include practical tips for optimal results
- Full GB translation support with British spelling consistency
- Integrate tooltip into main tool component

The tooltip helps users understand:
- What the tool does and when to use it
- How each technical parameter affects processing
- Best practices for scanning and image preparation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 23:23:17 +01:00
Anthony StirlingandClaude ce2065aec4 Add complete GB translations for scanner image split tool
- Add scannerImageSplit section with submit/title/error translations
- Update home.scannerImageSplit title and description to match registry expectations
- Ensure all React component translation keys are properly supported
- Maintain British spelling ("colour") in existing translations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 23:17:49 +01:00
Anthony StirlingandClaude e7109bb4e9 Convert extract-image-scans to React component
- Create full React implementation for scanner image split tool
- Add ScannerImageSplit component with settings and operation hooks
- Enhance zipFileService with extractAllFiles() method for general ZIP handling
- Add custom response handler to properly extract ZIP files containing images
- Integrate tool into registry with complete operation config
- Support both single image and ZIP responses from backend

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-25 23:13:08 +01:00
156 changed files with 1313 additions and 10707 deletions
-345
View File
@@ -1,345 +0,0 @@
"""
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,7 +12,6 @@ on:
branches:
- V2
paths:
- ".github/workflows/frontend-licenses-update.yml"
- "frontend/package.json"
- "frontend/package-lock.json"
- "frontend/scripts/generate-licenses.js"
@@ -29,12 +28,12 @@ jobs:
repository-projects: write # Required for enabling automerge
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
uses: step-security/harden-runner@6c439dc8bdf85cadbbce9ed30d1c7b959517bc49 # v2.12.2
with:
egress-policy: audit
- name: Checkout PR head (default)
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
persist-credentials: false
@@ -49,7 +48,7 @@ jobs:
- name: Checkout BASE branch (safe script)
if: github.event_name == 'pull_request'
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.base.sha }}
path: base
@@ -57,9 +56,9 @@ jobs:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
node-version: '22'
node-version: '18'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
@@ -115,7 +114,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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
@@ -168,7 +167,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@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
-118
View File
@@ -1,118 +0,0 @@
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
+36 -35
View File
@@ -97,6 +97,7 @@ 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:
@@ -115,46 +116,46 @@ Stirling-PDF currently supports 40 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![37%](https://geps.dev/progress/37) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![37%](https://geps.dev/progress/37) |
| Basque (Euskara) (eu_ES) | ![21%](https://geps.dev/progress/21) |
| Bulgarian (Български) (bg_BG) | ![40%](https://geps.dev/progress/40) |
| Catalan (Català) (ca_CA) | ![39%](https://geps.dev/progress/39) |
| Croatian (Hrvatski) (hr_HR) | ![36%](https://geps.dev/progress/36) |
| Czech (Česky) (cs_CZ) | ![39%](https://geps.dev/progress/39) |
| Danish (Dansk) (da_DK) | ![35%](https://geps.dev/progress/35) |
| Dutch (Nederlands) (nl_NL) | ![35%](https://geps.dev/progress/35) |
| Arabic (العربية) (ar_AR) | ![61%](https://geps.dev/progress/61) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![62%](https://geps.dev/progress/62) |
| Basque (Euskara) (eu_ES) | ![36%](https://geps.dev/progress/36) |
| Bulgarian (Български) (bg_BG) | ![68%](https://geps.dev/progress/68) |
| Catalan (Català) (ca_CA) | ![68%](https://geps.dev/progress/68) |
| Croatian (Hrvatski) (hr_HR) | ![60%](https://geps.dev/progress/60) |
| Czech (Česky) (cs_CZ) | ![70%](https://geps.dev/progress/70) |
| Danish (Dansk) (da_DK) | ![61%](https://geps.dev/progress/61) |
| Dutch (Nederlands) (nl_NL) | ![60%](https://geps.dev/progress/60) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![42%](https://geps.dev/progress/42) |
| French (Français) (fr_FR) | ![89%](https://geps.dev/progress/89) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![39%](https://geps.dev/progress/39) |
| Hindi (हिंदी) (hi_IN) | ![40%](https://geps.dev/progress/40) |
| Hungarian (Magyar) (hu_HU) | ![43%](https://geps.dev/progress/43) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![36%](https://geps.dev/progress/36) |
| Irish (Gaeilge) (ga_IE) | ![40%](https://geps.dev/progress/40) |
| Greek (Ελληνικά) (el_GR) | ![67%](https://geps.dev/progress/67) |
| Hindi (हिंदी) (hi_IN) | ![67%](https://geps.dev/progress/67) |
| Hungarian (Magyar) (hu_HU) | ![99%](https://geps.dev/progress/99) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![62%](https://geps.dev/progress/62) |
| Irish (Gaeilge) (ga_IE) | ![68%](https://geps.dev/progress/68) |
| Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![41%](https://geps.dev/progress/41) |
| Korean (한국어) (ko_KR) | ![40%](https://geps.dev/progress/40) |
| Norwegian (Norsk) (no_NB) | ![37%](https://geps.dev/progress/37) |
| Persian (فارسی) (fa_IR) | ![39%](https://geps.dev/progress/39) |
| Polish (Polski) (pl_PL) | ![41%](https://geps.dev/progress/41) |
| Portuguese (Português) (pt_PT) | ![40%](https://geps.dev/progress/40) |
| Portuguese Brazilian (Português) (pt_BR) | ![42%](https://geps.dev/progress/42) |
| Romanian (Română) (ro_RO) | ![33%](https://geps.dev/progress/33) |
| Russian (Русский) (ru_RU) | ![43%](https://geps.dev/progress/43) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![43%](https://geps.dev/progress/43) |
| Simplified Chinese (简体中文) (zh_CN) | ![99%](https://geps.dev/progress/99) |
| Slovakian (Slovensky) (sk_SK) | ![30%](https://geps.dev/progress/30) |
| Slovenian (Slovenščina) (sl_SI) | ![41%](https://geps.dev/progress/41) |
| Spanish (Español) (es_ES) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![38%](https://geps.dev/progress/38) |
| Thai (ไทย) (th_TH) | ![35%](https://geps.dev/progress/35) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![67%](https://geps.dev/progress/67) |
| Norwegian (Norsk) (no_NB) | ![66%](https://geps.dev/progress/66) |
| Persian (فارسی) (fa_IR) | ![64%](https://geps.dev/progress/64) |
| Polish (Polski) (pl_PL) | ![72%](https://geps.dev/progress/72) |
| Portuguese (Português) (pt_PT) | ![69%](https://geps.dev/progress/69) |
| Portuguese Brazilian (Português) (pt_BR) | ![76%](https://geps.dev/progress/76) |
| Romanian (Română) (ro_RO) | ![57%](https://geps.dev/progress/57) |
| Russian (Русский) (ru_RU) | ![88%](https://geps.dev/progress/88) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![95%](https://geps.dev/progress/95) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![51%](https://geps.dev/progress/51) |
| Slovenian (Slovenščina) (sl_SI) | ![71%](https://geps.dev/progress/71) |
| Spanish (Español) (es_ES) | ![74%](https://geps.dev/progress/74) |
| Swedish (Svenska) (sv_SE) | ![65%](https://geps.dev/progress/65) |
| Thai (ไทย) (th_TH) | ![59%](https://geps.dev/progress/59) |
| Tibetan (བོད་ཡིག་) (bo_CN) | ![65%](https://geps.dev/progress/65) |
| Traditional Chinese (繁體中文) (zh_TW) | ![43%](https://geps.dev/progress/43) |
| Turkish (Türkçe) (tr_TR) | ![43%](https://geps.dev/progress/43) |
| Ukrainian (Українська) (uk_UA) | ![42%](https://geps.dev/progress/42) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![32%](https://geps.dev/progress/32) |
| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) |
| Turkish (Türkçe) (tr_TR) | ![80%](https://geps.dev/progress/80) |
| Ukrainian (Українська) (uk_UA) | ![71%](https://geps.dev/progress/71) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![57%](https://geps.dev/progress/57) |
| Malayalam (മലയാളം) (ml_IN) | ![73%](https://geps.dev/progress/73) |
## Stirling PDF Enterprise
+1 -1
View File
@@ -4,7 +4,7 @@ FROM node:20-alpine AS build
WORKDIR /app
# Copy package files
COPY frontend/package.json frontend/package-lock.json ./
COPY frontend/package*.json ./
# Install dependencies
RUN npm ci
-13
View File
@@ -6,11 +6,6 @@ http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Add .mjs MIME type mapping
types {
text/javascript mjs;
}
# Gzip compression
gzip on;
gzip_vary on;
@@ -95,14 +90,6 @@ http {
proxy_set_header X-Forwarded-Port $server_port;
}
# Serve .mjs files with correct MIME type (must come before general static assets)
location ~* \.mjs$ {
try_files $uri =404;
add_header Content-Type "text/javascript; charset=utf-8" always;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
+110 -191
View File
@@ -10,24 +10,21 @@
"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.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",
"@embedpdf/core": "^1.2.1",
"@embedpdf/engines": "^1.2.1",
"@embedpdf/plugin-interaction-manager": "^1.2.1",
"@embedpdf/plugin-loader": "^1.2.1",
"@embedpdf/plugin-pan": "^1.2.1",
"@embedpdf/plugin-render": "^1.2.1",
"@embedpdf/plugin-rotate": "^1.2.1",
"@embedpdf/plugin-scroll": "^1.2.1",
"@embedpdf/plugin-search": "^1.2.1",
"@embedpdf/plugin-selection": "^1.2.1",
"@embedpdf/plugin-spread": "^1.2.1",
"@embedpdf/plugin-thumbnail": "^1.2.1",
"@embedpdf/plugin-tiling": "^1.2.1",
"@embedpdf/plugin-viewport": "^1.2.1",
"@embedpdf/plugin-zoom": "^1.2.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@iconify/react": "^6.0.2",
@@ -491,13 +488,12 @@
}
},
"node_modules/@embedpdf/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.3.1.tgz",
"integrity": "sha512-2Az6trhiMMBIv+GFvV8H8UOS1gwQn7NK0KaJMcdsZbUHYLO0P95aVd6Pi/GRzEH4XyF51TDIoTOAUtf07TQ5dQ==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/core/-/core-1.2.1.tgz",
"integrity": "sha512-2VwRPsN3+LmaBrD8TCN1t1ni/Vc9CxAfl/SApDjZYwE7zOieQT4ZHt+nkgF0F4I3xSgvvyHDjmOonhjBIrT6xA==",
"dependencies": {
"@embedpdf/engines": "1.3.1",
"@embedpdf/models": "1.3.1"
"@embedpdf/engines": "1.2.1",
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"preact": "^10.26.4",
@@ -507,13 +503,13 @@
}
},
"node_modules/@embedpdf/engines": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.3.1.tgz",
"integrity": "sha512-G3pI+18la7spviUMuA5s9/hV95jlfkA2+CNxqlHBO5ocw3641E3d36Lv+mx+6yU7k0B5vEOQPZDGRMg7KFziBQ==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/engines/-/engines-1.2.1.tgz",
"integrity": "sha512-nhycZ7Buq2B34dcpo6n7RdFwdhwTvKzvnRy7QX+uU00Dz5vftkCG4OK+pBVzxE4y7vAu+Yb4wNpdc7HmIj3B6w==",
"license": "MIT",
"dependencies": {
"@embedpdf/models": "1.3.1",
"@embedpdf/pdfium": "1.3.1"
"@embedpdf/models": "1.2.1",
"@embedpdf/pdfium": "1.2.1"
},
"peerDependencies": {
"preact": "^10.26.4",
@@ -523,79 +519,26 @@
}
},
"node_modules/@embedpdf/models": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.3.1.tgz",
"integrity": "sha512-OzmO1rQAuOP/Y3aYXmW21dPNAx49olhr9ZO2hDdI0fbNBHTVGxnaKqOISxVmUz7TmhTwVBljERACnaA8Ib4b4Q==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/models/-/models-1.2.1.tgz",
"integrity": "sha512-FzJU51jsqihfgt50B00FEpgyym87/Dn2iGmMq4++Vu/oO6qBx/y69m4/cCAh4p4KkTJsvKNWC7T7dwSKa0FjHA==",
"license": "MIT"
},
"node_modules/@embedpdf/pdfium": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.3.1.tgz",
"integrity": "sha512-qYGSS5ntz6DSY9Cxw/aigvHqGB+AKJLEcymNTZOL0GdlBzZpL++dOIYNEYHO2Tm/lOQVpE7I0e+Xh2TvD8O1zQ==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/pdfium/-/pdfium-1.2.1.tgz",
"integrity": "sha512-QWf1jg7EqUlku2q6KYhlXCNfk5IAykFerPuzKJepHTeAEaRcAfu84fJgEsoUTCK4D6dfzVNp2Iuxw6Kv7MpSeg==",
"license": "MIT"
},
"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.1",
"@embedpdf/utils": "1.3.1"
},
"peerDependencies": {
"@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",
"vue": ">=3.2.0"
}
},
"node_modules/@embedpdf/plugin-interaction-manager": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-interaction-manager/-/plugin-interaction-manager-1.2.1.tgz",
"integrity": "sha512-HhEBuDjDNMH6wu76Eo3yHwjG01U1lNZShkOsFoib/rtx8HByTgZS8iVpovaOprr6gfS04ZLqWcsN1nt5qAH90w==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/core": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -603,15 +546,14 @@
}
},
"node_modules/@embedpdf/plugin-loader": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-loader/-/plugin-loader-1.2.1.tgz",
"integrity": "sha512-VblKErfEiHcVao18TfCmc0UJlKAkqxE29DaLJrXQHGUw/qc+pC9HlvMVpDz3+Eb13UafYS6ZUZuEng2/fQ+JJw==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/core": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -619,17 +561,16 @@
}
},
"node_modules/@embedpdf/plugin-pan": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-pan/-/plugin-pan-1.2.1.tgz",
"integrity": "sha512-/BTOyRl31tvnCmoLs4qNPROMRLaG34jGYNyMQquB0uPUXZjwdMloikriwos91qCOLUrhvs4SaDpC3Ghv2BO5kA==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-interaction-manager": "1.3.1",
"@embedpdf/plugin-viewport": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-interaction-manager": "1.2.1",
"@embedpdf/plugin-viewport": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -637,15 +578,14 @@
}
},
"node_modules/@embedpdf/plugin-render": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-render/-/plugin-render-1.2.1.tgz",
"integrity": "sha512-iMfuVJqttJmm7Zb8oOaqNVNrC3NS57bDNNAc4MIc2f2TxIFSznvBPlwWN+PN45qNcTQiGzFc1ZMqIQDOG4qFnQ==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/core": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -653,15 +593,14 @@
}
},
"node_modules/@embedpdf/plugin-rotate": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-rotate/-/plugin-rotate-1.2.1.tgz",
"integrity": "sha512-UhHds5donLDXm3i9nKrhSmo3yawVtjb6gID0MDrhj3+Lci/YQ3wDvGUhk7dNmgLcOt7G8pMa0wesnnpVWirUXA==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/core": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -669,16 +608,15 @@
}
},
"node_modules/@embedpdf/plugin-scroll": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.3.1.tgz",
"integrity": "sha512-mDvK3DyBZC8/8pOEdJsWtSjCmV2ZuZJJ6xfspJpsaDVywo1Vq6M55BtKThkhqED6mqbFWTN9rP9cbWG8KDBWVA==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-scroll/-/plugin-scroll-1.2.1.tgz",
"integrity": "sha512-I1haDXIOzs59uhOWEP6UvP5jzjcQHMLQuQbfRVJM0zdWU6t3jwSfcwPUI7iv4CAAepbuyJKL328yc8736r/FYw==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-viewport": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-viewport": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -686,16 +624,15 @@
}
},
"node_modules/@embedpdf/plugin-search": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.3.1.tgz",
"integrity": "sha512-SLwYPQg1NJWytq2sd4MnWFmRVGgzwbohBedB2kH0ALsvdnoRYqgjR5HqAsKgoRJO/pphQhHlk3L1gLW62r6hqQ==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-search/-/plugin-search-1.2.1.tgz",
"integrity": "sha512-sl9FBQzbOBtdmPpf6UI0bnWCTPWDkj47rTxyK07bpnGfGuFof4zhcxmMaFdyP7zqBh4Y9XqGu4A0uMTO2d/t7g==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-loader": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-loader": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -703,17 +640,16 @@
}
},
"node_modules/@embedpdf/plugin-selection": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.3.1.tgz",
"integrity": "sha512-yef2XB/zR7zjyeUB3Ul0SbTcXqu5isR0GtINkFwL7bJMok6HpYNDnMXSuo55BaxI0dOCnnCSZfoRkAgosnZ1uQ==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-selection/-/plugin-selection-1.2.1.tgz",
"integrity": "sha512-wgG1X1sl6sed3pv7WLIO74SX0x3389/ax+/OLMty/LFbDNYMRO+n8ZQss8aUM700HARIqkPJy7UoSQt91o4nwA==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-interaction-manager": "1.3.1",
"@embedpdf/plugin-viewport": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-interaction-manager": "1.2.1",
"@embedpdf/plugin-viewport": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -721,16 +657,15 @@
}
},
"node_modules/@embedpdf/plugin-spread": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-spread/-/plugin-spread-1.2.1.tgz",
"integrity": "sha512-rpadnutT1wSdBQV7RQz40zYdKgCRgmJde/tamgB8oHQypcnZGQcAG6/ZfX5j12s9pG18hKwwL+KmMoBnXD9IjQ==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-loader": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-loader": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -738,34 +673,32 @@
}
},
"node_modules/@embedpdf/plugin-thumbnail": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-thumbnail/-/plugin-thumbnail-1.2.1.tgz",
"integrity": "sha512-TjHPkK8p3+FDMLcUdb3/4VREjm+liVooufLPVZ3FCXHbiC0PeUkqnwAxpCS2Jw1n+EtkY8pefRdRJeZhO6plOQ==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-render": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-render": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@embedpdf/plugin-tiling": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.3.1.tgz",
"integrity": "sha512-Q8RF80fb6y9GDAKwvgsu0BsWJlQuhNCtSKWwp3YcZJtIBFm94DVcg0zTgvDmE9/WNOmn4Z1Edt86usmYauHolw==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-tiling/-/plugin-tiling-1.2.1.tgz",
"integrity": "sha512-C9uOGVIsoxUw+uQMXfJFZ8ibRLQeNOnaKC2izjx967iGu0ZoecAv+mKtH/Ge0vMEKYM1109AlF5T2EGwKQW2YA==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/plugin-render": "1.3.1",
"@embedpdf/plugin-scroll": "1.3.1",
"@embedpdf/plugin-viewport": "1.3.1",
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-render": "1.2.1",
"@embedpdf/plugin-scroll": "1.2.1",
"@embedpdf/plugin-viewport": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -773,15 +706,14 @@
}
},
"node_modules/@embedpdf/plugin-viewport": {
"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",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-viewport/-/plugin-viewport-1.2.1.tgz",
"integrity": "sha512-yvftOis7FLBjM3w2VYO5LXVKXoHkmFV/SPy7U6SbuLJTX126F4ohSij9euMHJjaqOgr5tBNvrf4xemVRglxM9w==",
"dependencies": {
"@embedpdf/models": "1.3.1"
"@embedpdf/models": "1.2.1"
},
"peerDependencies": {
"@embedpdf/core": "1.3.1",
"@embedpdf/core": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
@@ -789,31 +721,18 @@
}
},
"node_modules/@embedpdf/plugin-zoom": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.3.1.tgz",
"integrity": "sha512-3GXpgv6XmZiQnjaPbsxblTqUn84ALFiyONh2gwrEU9apB6STT3TQiY0QRindwrUXdQLpCSjRSB9PpDBCtTww7w==",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@embedpdf/plugin-zoom/-/plugin-zoom-1.2.1.tgz",
"integrity": "sha512-hsp/nM4C8q0FM9P6FkpQLbU8IYawUgmiYgD3HXqHWBVRk30OIaXs4N0KC9vsHwn8ZAiyLl7jhlAXpgoacH5xEQ==",
"dependencies": {
"@embedpdf/models": "1.3.1",
"@embedpdf/models": "1.2.1",
"hammerjs": "^2.0.8"
},
"peerDependencies": {
"@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": {
"@embedpdf/core": "1.2.1",
"@embedpdf/plugin-interaction-manager": "1.2.1",
"@embedpdf/plugin-scroll": "1.2.1",
"@embedpdf/plugin-viewport": "1.2.1",
"preact": "^10.26.4",
"react": ">=16.8.0",
"react-dom": ">=16.8.0",
+15 -18
View File
@@ -6,24 +6,21 @@
"proxy": "http://localhost:8080",
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
"@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",
"@embedpdf/core": "^1.2.1",
"@embedpdf/engines": "^1.2.1",
"@embedpdf/plugin-interaction-manager": "^1.2.1",
"@embedpdf/plugin-loader": "^1.2.1",
"@embedpdf/plugin-pan": "^1.2.1",
"@embedpdf/plugin-render": "^1.2.1",
"@embedpdf/plugin-rotate": "^1.2.1",
"@embedpdf/plugin-scroll": "^1.2.1",
"@embedpdf/plugin-search": "^1.2.1",
"@embedpdf/plugin-selection": "^1.2.1",
"@embedpdf/plugin-spread": "^1.2.1",
"@embedpdf/plugin-thumbnail": "^1.2.1",
"@embedpdf/plugin-tiling": "^1.2.1",
"@embedpdf/plugin-viewport": "^1.2.1",
"@embedpdf/plugin-zoom": "^1.2.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@iconify/react": "^6.0.2",
+81 -308
View File
@@ -87,10 +87,7 @@
"showStack": "Stack-Trace anzeigen",
"copyStack": "Stack-Trace kopieren",
"githubSubmit": "GitHub - Ein Ticket 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."
"discordSubmit": "Discord - Unterstützungsbeitrag einreichen"
},
"warning": {
"tooltipTitle": "Warnung"
@@ -361,223 +358,179 @@
"sortBy": "Sortieren nach:",
"multiTool": {
"title": "PDF-Multitool",
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen",
"tags": "mehrere,werkzeuge"
"desc": "Seiten zusammenführen, drehen, neu anordnen und entfernen"
},
"merge": {
"title": "Zusammenführen",
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen",
"tags": "kombinieren,zusammenführen,vereinen"
"desc": "Mehrere PDF-Dateien zu einer einzigen zusammenführen"
},
"split": {
"title": "Aufteilen",
"desc": "PDFs in mehrere Dokumente aufteilen",
"tags": "teilen,trennen,aufteilen"
"desc": "PDFs in mehrere Dokumente aufteilen"
},
"rotate": {
"title": "Drehen",
"desc": "Drehen Sie Ihre PDFs ganz einfach",
"tags": "drehen,spiegeln,ausrichten"
"desc": "Drehen Sie Ihre PDFs ganz einfach"
},
"convert": {
"title": "Umwandeln",
"desc": "Dateien zwischen verschiedenen Formaten konvertieren",
"tags": "umwandeln,ändern"
"desc": "Dateien zwischen verschiedenen Formaten konvertieren"
},
"pdfOrganiser": {
"title": "Organisieren",
"desc": "Seiten entfernen und Seitenreihenfolge ändern",
"tags": "organisieren,umordnen,neu anordnen"
"desc": "Seiten entfernen und Seitenreihenfolge ändern"
},
"addImage": {
"title": "Bild einfügen",
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)",
"tags": "einfügen,einbetten,platzieren"
"desc": "Fügt ein Bild an eine bestimmte Stelle im PDF ein (in Arbeit)"
},
"addAttachments": {
"title": "Anhänge hinzufügen",
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen",
"tags": "einbetten,anhängen,einfügen"
"desc": "Eingebettete Dateien (Anhänge) zu einer PDF hinzufügen oder entfernen"
},
"watermark": {
"title": "Wasserzeichen hinzufügen",
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu",
"tags": "stempel,markierung,überlagerung"
"desc": "Fügen Sie ein eigenes Wasserzeichen zu Ihrem PDF hinzu"
},
"removePassword": {
"title": "Passwort entfernen",
"desc": "Den Passwortschutz eines PDFs entfernen",
"tags": "entsperren"
"desc": "Den Passwortschutz eines PDFs entfernen"
},
"compress": {
"title": "Komprimieren",
"desc": "PDF komprimieren um die Dateigröße zu reduzieren",
"tags": "verkleinern,reduzieren,optimieren"
"desc": "PDF komprimieren um die Dateigröße zu reduzieren"
},
"unlockPDFForms": {
"title": "Schreibgeschützte PDF-Formfelder entfernen",
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument.",
"tags": "entsperren,aktivieren,bearbeiten"
"desc": "Entfernen Sie die schreibgeschützte Eigenschaft von Formularfeldern in einem PDF-Dokument."
},
"changeMetadata": {
"title": "Metadaten ändern",
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument",
"tags": "bearbeiten,ändern,aktualisieren"
"desc": "Ändern/Entfernen/Hinzufügen von Metadaten aus einem PDF-Dokument"
},
"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",
"tags": "extrahieren,scannen"
"desc": "Cleanup scannt und erkennt Text aus Bildern in einer PDF-Datei und fügt ihn erneut als Text hinzu"
},
"extractImages": {
"title": "Bilder extrahieren",
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv",
"tags": "extrahieren,speichern,exportieren"
"desc": "Extrahiert alle Bilder aus einer PDF-Datei und speichert sie als Zip-Archiv"
},
"scannerImageSplit": {
"title": "Gescannte Fotos erkennen/aufteilen",
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf",
"tags": "erkennen,teilen,fotos"
"desc": "Teilt mehrere Fotos aus einem Foto/PDF auf"
},
"sign": {
"title": "Signieren",
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu",
"tags": "unterschrift,autogramm"
"desc": "Fügt PDF-Signaturen durch Zeichnung, Text oder Bild hinzu"
},
"flatten": {
"title": "Abflachen",
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen",
"tags": "vereinfachen,entfernen,interaktiv"
"desc": "Alle interaktiven Elemente und Formulare aus einem PDF entfernen"
},
"certSign": {
"title": "Mit Zertifikat 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"
"desc": "Ein PDF mit einem Zertifikat/Schlüssel (PEM/P12) signieren"
},
"repair": {
"title": "Reparatur",
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren",
"tags": "reparieren,wiederherstellen"
"desc": "Versucht, ein beschädigtes/kaputtes PDF zu reparieren"
},
"removeBlanks": {
"title": "Leere Seiten entfernen",
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument",
"tags": "löschen,bereinigen,leer"
"desc": "Erkennt und entfernt leere Seiten aus einem Dokument"
},
"removeAnnotations": {
"title": "Anmerkungen entfernen",
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF",
"tags": "löschen,bereinigen,entfernen"
"desc": "Entfernt alle Kommentare/Anmerkungen aus einem PDF"
},
"compare": {
"title": "Vergleichen",
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an",
"tags": "unterschied"
"desc": "Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an"
},
"removeCertSign": {
"title": "Zertifikatsignatur entfernen",
"desc": "Zertifikatsignatur aus PDF entfernen",
"tags": "entfernen,löschen,entsperren"
"desc": "Zertifikatsignatur aus PDF entfernen"
},
"pageLayout": {
"title": "Mehrseitiges Layout",
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen",
"tags": "layout,anordnen,kombinieren"
"desc": "Mehrere Seiten eines PDF zu einer Seite zusammenführen"
},
"bookletImposition": {
"title": "Broschüren-Layout",
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen",
"tags": "broschüre,druck,bindung"
"desc": "Broschüren mit korrekter Seitenreihenfolge und mehrseitigem Layout für Druck und Bindung erstellen"
},
"scalePages": {
"title": "Seitengröße/Skalierung anpassen",
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern",
"tags": "größe ändern,anpassen,skalieren"
"desc": "Größe/Skalierung der Seite und/oder des Inhalts ändern"
},
"addPageNumbers": {
"title": "Seitenzahlen hinzufügen",
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle",
"tags": "nummerieren,paginierung,zählen"
"desc": "Hinzufügen von Seitenzahlen an einer bestimmten Stelle"
},
"autoRename": {
"title": "PDF-Datei automatisch umbenennen",
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um",
"tags": "auto-erkennung,kopfzeilen-basiert,organisieren,umbenennen"
"desc": "Benennt eine PDF-Datei automatisch basierend auf der erkannten Überschrift um"
},
"adjustContrast": {
"title": "Farben/Kontrast anpassen",
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen",
"tags": "kontrast,helligkeit,sättigung"
"desc": "Kontrast, Sättigung und Helligkeit einer PDF anpassen"
},
"crop": {
"title": "PDF zuschneiden",
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)",
"tags": "zuschneiden,schneiden,größe ändern"
"desc": "PDF zuschneiden um die Größe zu verändern (Text bleibt erhalten!)"
},
"autoSplitPDF": {
"title": "PDF automatisch teilen",
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen",
"tags": "auto,teilen,QR"
"desc": "Physisch gescannte PDF anhand von Splitter-Seiten und QR-Codes aufteilen"
},
"sanitize": {
"title": "Bereinigen",
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen",
"tags": "bereinigen,löschen,entfernen"
"desc": "Potentiell schädliche Elemente aus PDF-Dateien entfernen"
},
"getPdfInfo": {
"title": "Alle Informationen anzeigen",
"desc": "Erfasst alle möglichen Informationen in einer PDF",
"tags": "info,metadaten,details"
"desc": "Erfasst alle möglichen Informationen in einer PDF"
},
"pdfToSinglePage": {
"title": "PDF zu einer Seite zusammenfassen",
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen",
"tags": "kombinieren,zusammenführen,einzeln"
"desc": "Fügt alle PDF-Seiten zu einer einzigen großen Seite zusammen"
},
"showJS": {
"title": "Javascript anzeigen",
"desc": "Alle Javascript Funktionen in einer PDF anzeigen",
"tags": "javascript,code,skript"
"desc": "Alle Javascript Funktionen in einer PDF anzeigen"
},
"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)",
"tags": "zensieren,schwärzen,verbergen"
"desc": "Zensiere (Schwärze) eine PDF-Datei durch Auswählen von Text, gezeichneten Formen und/oder ausgewählten Seite(n)"
},
"overlayPdfs": {
"title": "PDFs überlagern",
"desc": "PDFs über eine andere PDF überlagern",
"tags": "überlagern,kombinieren,stapeln"
"desc": "PDFs über eine andere PDF überlagern"
},
"splitBySections": {
"title": "PDF nach Abschnitten aufteilen",
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen",
"tags": "teilen,abschnitte,aufteilen"
"desc": "Jede Seite einer PDF in kleinere horizontale und vertikale Abschnitte unterteilen"
},
"addStamp": {
"title": "Stempel zu PDF hinzufügen",
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen",
"tags": "stempel,markierung,siegel"
"desc": "Text- oder Bildstempel an festgelegten Positionen hinzufügen"
},
"removeImage": {
"title": "Bild entfernen",
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern",
"tags": "entfernen,löschen,bereinigen"
"desc": "Bild aus PDF entfernen, um die Dateigröße zu verringern"
},
"splitByChapters": {
"title": "PDF-Datei nach Kapiteln aufteilen",
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.",
"tags": "teilen,kapitel,struktur"
"desc": "Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur."
},
"validateSignature": {
"title": "PDF-Signatur überprüfen",
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen",
"tags": "validieren,überprüfen,zertifikat"
"desc": "Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen"
},
"swagger": {
"title": "API-Dokumentation",
"desc": "API-Dokumentation anzeigen und Endpunkte testen",
"tags": "API,dokumentation,test"
"desc": "API-Dokumentation anzeigen und Endpunkte testen"
},
"fakeScan": {
"title": "Scan simulieren",
@@ -585,52 +538,42 @@
},
"editTableOfContents": {
"title": "Inhaltsverzeichnis bearbeiten",
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten",
"tags": "lesezeichen,inhalt,bearbeiten"
"desc": "Hinzufügen oder Bearbeiten von Lesezeichen und Inhaltsverzeichnissen in PDF-Dokumenten"
},
"manageCertificates": {
"title": "Zertifikate verwalten",
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen.",
"tags": "zertifikate,importieren,exportieren"
"desc": "Digitale Zertifikatsdateien für die PDF-Signierung importieren, exportieren oder löschen."
},
"read": {
"title": "Lesen",
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen.",
"tags": "anzeigen,öffnen,anzeigen"
"desc": "PDFs anzeigen und kommentieren. Text hervorheben, zeichnen oder Kommentare für Überprüfung und Zusammenarbeit einfügen."
},
"reorganizePages": {
"title": "Seiten neu anordnen",
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen.",
"tags": "umordnen,neu anordnen,organisieren"
"desc": "PDF-Seiten mit visueller Drag-and-Drop-Steuerung neu anordnen, duplizieren oder löschen."
},
"extractPages": {
"title": "Seiten extrahieren",
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren",
"tags": "extrahieren,auswählen,kopieren"
"desc": "Spezifische Seiten aus einem PDF-Dokument extrahieren"
},
"removePages": {
"title": "Entfernen",
"desc": "Ungewollte Seiten aus dem PDF entfernen",
"tags": "löschen,extrahieren,ausschließen"
"desc": "Ungewollte Seiten aus dem PDF entfernen"
},
"autoSizeSplitPDF": {
"title": "Teilen nach Größe/Anzahl",
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf",
"tags": "auto,teilen,größe"
"desc": "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf"
},
"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",
"tags": "API,entwicklung,dokumentation",
"title": "API"
"desc": "Link zur API-Dokumentation"
},
"devFolderScanning": {
"title": "Automatische Ordnerüberwachung",
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen",
"tags": "automatisierung,ordner,scannen"
"desc": "Link zum Leitfaden für automatisches Ordner-Scannen"
},
"devSsoGuide": {
"title": "SSO-Anleitung",
@@ -650,17 +593,7 @@
},
"automate": {
"title": "Automatisieren",
"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"
"desc": "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben."
}
},
"landing": {
@@ -700,18 +633,8 @@
"merge": {
"tags": "zusammenführen,seitenvorgänge,back end,serverseitig",
"title": "Zusammenführen",
"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"
}
},
"removeDigitalSignature": "Digitale Signatur in der zusammengeführten Datei entfernen?",
"generateTableOfContents": "Inhaltsverzeichnis in der zusammengeführten Datei erstellen?",
"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.",
@@ -937,13 +860,7 @@
"images": "Bilder",
"officeDocs": "Office-Dokumente (Word, Excel, PowerPoint)",
"imagesExt": "Bilder (JPG, PNG, usw.)",
"grayscale": "Graustufen",
"dpi": "DPI",
"markdown": "Markdown",
"odtExt": "OpenDocument Text (.odt)",
"pptExt": "PowerPoint (.pptx)",
"rtfExt": "Rich Text Format (.rtf)",
"textRtf": "Text/RTF"
"grayscale": "Graustufen"
},
"imageToPdf": {
"tags": "konvertierung,img,jpg,bild,foto"
@@ -983,20 +900,7 @@
"10": "Ungerade-Gerade-Zusammenführung",
"11": "Alle Seiten duplizieren"
},
"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)."
}
"placeholder": "(z.B. 1,3,2 oder 4-8,2,10-12 oder 2n-1)"
},
"addImage": {
"tags": "img,jpg,bild,foto",
@@ -1025,8 +929,7 @@
"failed": "Ein Fehler ist beim Hinzufügen des Wasserzeichens zur PDF aufgetreten."
},
"watermarkType": {
"image": "Bild",
"text": "Text"
"image": "Bild"
},
"settings": {
"type": "Wasserzeichen-Typ",
@@ -1430,9 +1333,7 @@
},
"trapped": {
"label": "Trapped-Status",
"unknown": "Unbekannt",
"false": "Falsch",
"true": "Wahr"
"unknown": "Unbekannt"
},
"advanced": {
"title": "Erweiterte Optionen"
@@ -1621,13 +1522,7 @@
"header": "Bilder extrahieren",
"selectText": "Wählen Sie das Bildformat aus, in das extrahierte Bilder konvertiert werden sollen",
"allowDuplicates": "Doppelte Bilder speichern",
"submit": "Extrahieren",
"error": {
"failed": "Beim Extrahieren der Bilder aus der PDF ist ein Fehler aufgetreten."
},
"settings": {
"title": "Einstellungen"
}
"submit": "Extrahieren"
},
"pdfToPDFA": {
"tags": "archiv,langfristig,standard,konvertierung,speicherung,aufbewahrung",
@@ -1704,14 +1599,8 @@
"title": "Signieren",
"header": "PDFs signieren",
"upload": "Bild hochladen",
"draw": {
"clear": "Löschen",
"title": "Zeichnen Sie Ihre Unterschrift"
},
"text": {
"name": "Name des Unterzeichners",
"placeholder": "Geben Sie Ihren vollständigen Namen ein"
},
"draw": "Signatur zeichnen",
"text": "Texteingabe",
"clear": "Leeren",
"add": "Signieren",
"saved": "Gespeicherte Signaturen",
@@ -1727,35 +1616,7 @@
"previous": "Vorherige Seite",
"maintainRatio": "Seitenverhältnis beibehalten ein-/ausschalten",
"undo": "Rückgängig",
"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"
}
"redo": "Wiederherstellen"
},
"flatten": {
"tags": "statisch,deaktivieren,nicht interaktiv,optimieren",
@@ -1774,8 +1635,7 @@
"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.",
"flattenOnlyForms": "Nur Formulare vereinfachen"
"note": "Das Abflachen entfernt interaktive Elemente aus der PDF und macht sie nicht mehr bearbeitbar."
},
"results": {
"title": "Reduzierungs-Ergebnisse"
@@ -1833,8 +1693,7 @@
"label": "Pixel-Weißheitsschwellwert"
},
"whitePercent": {
"label": "Weiß-Prozentsatz-Schwellwert",
"unit": "%"
"label": "Weiß-Prozentsatz-Schwellwert"
},
"includeBlankPages": {
"label": "Erkannte leere Seiten einschließen"
@@ -1871,17 +1730,7 @@
"tags": "kommentare,hervorheben,notizen,markieren,entfernen",
"title": "Kommentare entfernen",
"header": "Kommentare 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"
}
"submit": "Entfernen"
},
"compare": {
"tags": "differenzieren,kontrastieren,verändern,analysieren",
@@ -2166,9 +2015,7 @@
},
"pageSize": {
"label": "Ziel-Seitengröße",
"keep": "Ursprüngliche Größe beibehalten",
"legal": "Legal",
"letter": "Letter"
"keep": "Ursprüngliche Größe beibehalten"
},
"submit": "Seitenskalierung anpassen",
"error": {
@@ -2459,8 +2306,7 @@
"showLayers": "Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzustand zurückzusetzen)",
"colourPicker": "Farbwähler",
"findCurrentOutlineItem": "Aktuelles Gliederungselement finden",
"applyChanges": "Änderungen anwenden",
"zoom": "Zoom"
"applyChanges": "Änderungen anwenden"
}
},
"tableExtraxt": {
@@ -2650,8 +2496,7 @@
"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}}",
"debug": "Debug"
"unexpectedError": "Unerwarteter Fehler: {{message}}"
},
"signup": {
"title": "Konto erstellen",
@@ -2673,8 +2518,7 @@
"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}}",
"name": "Name"
"unexpectedError": "Unerwarteter Fehler: {{message}}"
},
"pdfToSinglePage": {
"title": "PDF zu einer Seite zusammenfassen",
@@ -3117,12 +2961,7 @@
"selectedCount": "{{count}} ausgewählt",
"download": "Herunterladen",
"delete": "Löschen",
"unsupported": "Nicht unterstützt",
"fileFormat": "Format",
"fileName": "Name",
"fileVersion": "Version",
"googleDrive": "Google Drive",
"googleDriveShort": "Drive"
"unsupported": "Nicht unterstützt"
},
"storage": {
"temporaryNotice": "Dateien werden temporär in Ihrem Browser gespeichert und können automatisch gelöscht werden",
@@ -3153,24 +2992,12 @@
"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": {
"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"
}
"removeJavaScript": "JavaScript entfernen",
"removeEmbeddedFiles": "Eingebettete Dateien entfernen",
"removeXMPMetadata": "XMP-Metadaten entfernen",
"removeMetadata": "Dokument-Metadaten entfernen",
"removeLinks": "Links entfernen",
"removeFonts": "Schriftarten entfernen"
}
},
"addPassword": {
@@ -3198,8 +3025,7 @@
"keyLength": {
"label": "Verschlüsselungsschlüssellänge",
"40bit": "40-bit (Niedrig)",
"256bit": "256-bit (Hoch)",
"128bit": "128-bit (Standard)"
"256bit": "256-bit (Hoch)"
}
},
"results": {
@@ -3438,58 +3264,5 @@
},
"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"
}
}
+31 -164
View File
@@ -1,10 +1,4 @@
{
"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"
},
@@ -16,34 +10,16 @@
"selectText": {
"1": "Select PDF file:",
"2": "Margin Size",
"3": "Position Selection",
"3": "Position",
"4": "Starting Number",
"5": "Pages to Number",
"6": "Custom Text Format"
"6": "Custom Text"
},
"customTextDesc": "Custom Text",
"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."
"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"
},
"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",
@@ -603,9 +579,9 @@
"title": "API Documentation",
"desc": "View API documentation and test endpoints"
},
"scannerEffect": {
"fakeScan": {
"tags": "scan,simulate,create",
"title": "Scanner Effect",
"title": "Fake Scan",
"desc": "Create a PDF that looks like it was scanned"
},
"editTableOfContents": {
@@ -643,7 +619,8 @@
"title": "Auto Split by Size/Count",
"desc": "Automatically split PDFs by file size or page count"
},
"replaceColor": {
"replaceColorPdf": {
"tags": "color,replace,invert",
"title": "Replace & Invert Colour",
"desc": "Replace or invert colours in PDF documents"
},
@@ -982,7 +959,7 @@
"header": "PDF Page Organiser",
"submit": "Rearrange Pages",
"mode": {
"_value": "Organization mode",
"_value": "Mode",
"1": "Custom Page Order",
"2": "Reverse Order",
"3": "Duplex Sort",
@@ -995,19 +972,6 @@
"10": "Odd-Even Merge",
"11": "Duplicate all pages"
},
"desc": {
"CUSTOM": "Use a custom sequence of page numbers or expressions to define a new order.",
"REVERSE_ORDER": "Flip the document so the last page becomes first and so on.",
"DUPLEX_SORT": "Interleave fronts then backs as if a duplex scanner scanned all fronts, then all backs (1, n, 2, n-1, …).",
"BOOKLET_SORT": "Arrange pages for booklet printing (last, first, second, second last, …).",
"SIDE_STITCH_BOOKLET_SORT": "Arrange pages for sidestitch booklet printing (optimised for binding on the side).",
"ODD_EVEN_SPLIT": "Split the document into two outputs: all odd pages and all even pages.",
"ODD_EVEN_MERGE": "Merge two PDFs by alternating pages: odd from the first, even from the second.",
"DUPLICATE": "Duplicate each page according to the custom order count (e.g., 4 duplicates each page 4×).",
"REMOVE_FIRST": "Remove the first page from the document.",
"REMOVE_LAST": "Remove the last page from the document.",
"REMOVE_FIRST_AND_LAST": "Remove both the first and last pages from the document."
},
"placeholder": "(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)"
},
"addImage": {
@@ -1632,13 +1596,7 @@
"header": "Extract Images",
"selectText": "Select image format to convert extracted images to",
"allowDuplicates": "Save duplicate images",
"submit": "Extract",
"settings": {
"title": "Settings"
},
"error": {
"failed": "An error occurred while extracting images from the PDF."
}
"submit": "Extract"
},
"pdfToPDFA": {
"tags": "archive,long-term,standard,conversion,storage,preservation",
@@ -1750,7 +1708,6 @@
"add": "Add",
"saved": "Saved Signatures",
"save": "Save Signature",
"applySignatures": "Apply Signatures",
"personalSigs": "Personal Signatures",
"sharedSigs": "Shared Signatures",
"noSavedSigs": "No saved signatures found",
@@ -1762,42 +1719,7 @@
"previous": "Previous page",
"maintainRatio": "Toggle maintain aspect ratio",
"undo": "Undo",
"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."
}
"redo": "Redo"
},
"flatten": {
"title": "Flatten",
@@ -1911,17 +1833,7 @@
"tags": "comments,highlight,notes,markup,remove",
"title": "Remove Annotations",
"header": "Remove Annotations",
"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."
}
"submit": "Remove"
},
"compare": {
"tags": "differentiate,contrast,changes,analysis",
@@ -2621,48 +2533,25 @@
},
"selectCustomCert": "Custom Certificate File X.509 (Optional)"
},
"replaceColor": {
"labels": {
"settings": "Settings",
"colourOperation": "Colour operation"
"replace-color": {
"title": "Advanced Colour options",
"header": "Replace-Invert Colour PDF",
"selectText": {
"1": "Replace or Invert colour Options",
"2": "Default(Default high contrast colours)",
"3": "Custom(Customised colours)",
"4": "Full-Invert(Invert all colours)",
"5": "High contrast colour options",
"6": "white text on black background",
"7": "Black text on white background",
"8": "Yellow text on black background",
"9": "Green text on black background",
"10": "Choose text Colour",
"11": "Choose background Colour"
},
"options": {
"highContrast": "High contrast",
"invertAll": "Invert all colours",
"custom": "Custom"
},
"tooltip": {
"header": {
"title": "Replace & Invert Colour Settings Overview"
},
"description": {
"title": "Description",
"text": "Transform PDF colours to improve readability and accessibility. Choose from high contrast presets, invert all colours, or create custom colour schemes."
},
"highContrast": {
"title": "High Contrast",
"text": "Apply predefined high contrast colour combinations designed for better readability and accessibility compliance.",
"bullet1": "White text on black background - Classic dark mode",
"bullet2": "Black text on white background - Standard high contrast",
"bullet3": "Yellow text on black background - High visibility option",
"bullet4": "Green text on black background - Alternative high contrast"
},
"invertAll": {
"title": "Invert All Colours",
"text": "Completely invert all colours in the PDF, creating a negative-like effect. Useful for creating dark mode versions of documents or reducing eye strain in low-light conditions."
},
"custom": {
"title": "Custom Colours",
"text": "Define your own text and background colours using the colour pickers. Perfect for creating branded documents or specific accessibility requirements.",
"bullet1": "Text colour - Choose the colour for text elements",
"bullet2": "Background colour - Set the background colour for the document"
}
},
"error": {
"failed": "An error occurred while processing the colour replacement."
}
"submit": "Replace"
},
"replaceColor": {
"replaceColorPdf": {
"tags": "Replace Colour,Page operations,Back end,server side"
},
"login": {
@@ -3439,16 +3328,6 @@
"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!",
@@ -3505,18 +3384,6 @@
"generateError": "We couldn't generate your API key."
}
},
"AddAttachmentsRequest": {
"attachments": "Select Attachments",
"info": "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.",
"selectFiles": "Select Files to Attach",
"placeholder": "Choose files...",
"addMoreFiles": "Add more files...",
"selectedFiles": "Selected Files",
"submit": "Add Attachments",
"results": {
"title": "Attachment Results"
}
},
"termsAndConditions": "Terms & Conditions",
"logOut": "Log out"
}
}
+9 -61
View File
@@ -769,7 +769,7 @@
"header": "PDF Page Organizer",
"submit": "Rearrange Pages",
"mode": {
"_value": "Organization mode",
"_value": "Mode",
"1": "Custom Page Order",
"2": "Reverse Order",
"3": "Duplex Sort",
@@ -782,19 +782,6 @@
"10": "Odd-Even Merge",
"11": "Duplicate all pages"
},
"desc": {
"CUSTOM": "Use a custom sequence of page numbers or expressions to define a new order.",
"REVERSE_ORDER": "Flip the document so the last page becomes first and so on.",
"DUPLEX_SORT": "Interleave fronts then backs as if a duplex scanner scanned all fronts, then all backs (1, n, 2, n-1, …).",
"BOOKLET_SORT": "Arrange pages for booklet printing (last, first, second, second last, …).",
"SIDE_STITCH_BOOKLET_SORT": "Arrange pages for sidestitch booklet printing (optimized for binding on the side).",
"ODD_EVEN_SPLIT": "Split the document into two outputs: all odd pages and all even pages.",
"ODD_EVEN_MERGE": "Merge two PDFs by alternating pages: odd from the first, even from the second.",
"DUPLICATE": "Duplicate each page according to the custom order count (e.g., 4 duplicates each page 4×).",
"REMOVE_FIRST": "Remove the first page from the document.",
"REMOVE_LAST": "Remove the last page from the document.",
"REMOVE_FIRST_AND_LAST": "Remove both the first and last pages from the document."
},
"placeholder": "(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)"
},
"addImage": {
@@ -1069,13 +1056,7 @@
"header": "Extract Images",
"selectText": "Select image format to convert extracted images to",
"allowDuplicates": "Save duplicate images",
"submit": "Extract",
"settings": {
"title": "Settings"
},
"error": {
"failed": "An error occurred while extracting images from the PDF."
}
"submit": "Extract"
},
"pdfToPDFA": {
"tags": "archive,long-term,standard,conversion,storage,preservation",
@@ -1243,17 +1224,7 @@
"tags": "comments,highlight,notes,markup,remove",
"title": "Remove Annotations",
"header": "Remove Annotations",
"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."
}
"submit": "Remove"
},
"compare": {
"tags": "differentiate,contrast,changes,analysis",
@@ -1833,16 +1804,10 @@
}
},
"removeImage": {
"title": "Remove Images",
"header": "Remove Images",
"removeImage": "Remove Images",
"submit": "Remove Images",
"results": {
"title": "Remove Images Results"
},
"error": {
"failed": "Failed to remove images from the PDF."
}
"title": "Remove image",
"header": "Remove image",
"removeImage": "Remove image",
"submit": "Remove image"
},
"splitByChapters": {
"title": "Split PDF by Chapters",
@@ -1887,7 +1852,7 @@
"title": "How we use Cookies",
"description": {
"1": "We use cookies and other technologies to make Stirling PDF work better for you—helping us improve our tools and keep building features you'll love.",
"2": "If you'd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
"2": "If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly."
},
"acceptAllBtn": "Okay",
"acceptNecessaryBtn": "No Thanks",
@@ -1911,7 +1876,7 @@
"1": "Strictly Necessary Cookies",
"2": "Always Enabled"
},
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can't be turned off."
"description": "These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off."
},
"analytics": {
"title": "Analytics",
@@ -2391,22 +2356,5 @@
},
"automate": {
"copyToSaved": "Copy to Saved"
},
"AddAttachmentsRequest": {
"attachments": "Select Attachments",
"info": "Select files to attach to your PDF. These files will be embedded and accessible through the PDF's attachment panel.",
"selectFiles": "Select Files to Attach",
"placeholder": "Choose files...",
"addMoreFiles": "Add more files...",
"selectedFiles": "Selected Files",
"submit": "Add Attachments",
"results": {
"title": "Attachment Results"
}
},
"addAttachments": {
"error": {
"failed": "An error occurred while adding attachments to the PDF."
}
}
}
File diff suppressed because it is too large Load Diff
+66 -255
View File
@@ -87,10 +87,7 @@
"showStack": "Mostra traccia dello stack",
"copyStack": "Copia traccia dello stack",
"githubSubmit": "GitHub: apri un ticket",
"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."
"discordSubmit": "Discord: invia post di supporto"
},
"warning": {
"tooltipTitle": "Avviso"
@@ -361,223 +358,179 @@
"sortBy": "Ordinamento:",
"multiTool": {
"title": "Multifunzione PDF",
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine",
"tags": "multipli,strumenti"
"desc": "Unisci, Ruota, Riordina, e Rimuovi pagine"
},
"merge": {
"title": "Unisci",
"desc": "Unisci facilmente più PDF in uno.",
"tags": "combina,unisci,unifica"
"desc": "Unisci facilmente più PDF in uno."
},
"split": {
"title": "Dividi",
"desc": "Dividi un singolo PDF in più documenti.",
"tags": "dividi,separa,spezza"
"desc": "Dividi un singolo PDF in più documenti."
},
"rotate": {
"title": "Ruota",
"desc": "Ruota un PDF.",
"tags": "ruota,capovolgi,orienta"
"desc": "Ruota un PDF."
},
"convert": {
"title": "Converti",
"desc": "Converti file tra diversi formati",
"tags": "trasforma,cambia"
"desc": "Converti file tra diversi formati"
},
"pdfOrganiser": {
"title": "Organizza",
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine.",
"tags": "organizza,riordina,riorganizza"
"desc": "Rimuovi/Riordina le pagine in qualsiasi ordine."
},
"addImage": {
"title": "Aggiungi Immagine",
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)",
"tags": "inserisci,incorpora,posiziona"
"desc": "Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso)"
},
"addAttachments": {
"title": "Aggiungi allegati",
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF",
"tags": "incorpora,allega,includi"
"desc": "Aggiungi o rimuovi file incorporati (allegati) da/verso un PDF"
},
"watermark": {
"title": "Aggiungi Filigrana",
"desc": "Aggiungi una filigrana al tuo PDF.",
"tags": "timbro,marca,sovrapponi"
"desc": "Aggiungi una filigrana al tuo PDF."
},
"removePassword": {
"title": "Rimuovi Password",
"desc": "Rimuovi la password dal tuo PDF.",
"tags": "sblocca"
"desc": "Rimuovi la password dal tuo PDF."
},
"compress": {
"title": "Comprimi",
"desc": "Comprimi PDF per ridurne le dimensioni.",
"tags": "riduci,comprimi,ottimizza"
"desc": "Comprimi PDF per ridurne le dimensioni."
},
"unlockPDFForms": {
"title": "Sblocca moduli PDF",
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF.",
"tags": "sblocca,abilita,modifica"
"desc": "Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF."
},
"changeMetadata": {
"title": "Modifica Proprietà",
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.",
"tags": "modifica,cambia,aggiorna"
"desc": "Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF."
},
"ocr": {
"title": "OCR / Pulisci scansioni",
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro.",
"tags": "estrai,scansiona"
"desc": "Pulisci scansioni ed estrai testo da immagini, convertendo le immagini in testo puro."
},
"extractImages": {
"title": "Estrai immagini",
"desc": "Estrai tutte le immagini da un PDF e salvale come zip.",
"tags": "estrai,salva,esporta"
"desc": "Estrai tutte le immagini da un PDF e salvale come zip."
},
"scannerImageSplit": {
"title": "Rileva/Dividi foto scansionate",
"desc": "Divide più foto allinterno di una foto/PDF",
"tags": "rileva,dividi,foto"
"desc": "Divide più foto allinterno di una foto/PDF"
},
"sign": {
"title": "Firma",
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine.",
"tags": "firma,autografo"
"desc": "Aggiungi una firma al PDF da disegno, testo o immagine."
},
"flatten": {
"title": "Appiattisci",
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF.",
"tags": "semplifica,rimuovi,interattivo"
"desc": "Rimuovi tutti gli elementi interattivi e moduli da un PDF."
},
"certSign": {
"title": "Firma con certificato",
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)",
"tags": "autentica,PEM,P12,ufficiale,cripta,firma,certificato,PKCS12,JKS,server,manuale,auto"
"desc": "Firma un PDF con un certificato/chiave (PEM/P12)"
},
"repair": {
"title": "Ripara",
"desc": "Prova a riparare un PDF corrotto.",
"tags": "ripara,ripristina"
"desc": "Prova a riparare un PDF corrotto."
},
"removeBlanks": {
"title": "Rimuovi pagine vuote",
"desc": "Trova e rimuovi pagine vuote da un PDF.",
"tags": "elimina,pulisci,vuote"
"desc": "Trova e rimuovi pagine vuote da un PDF."
},
"removeAnnotations": {
"title": "Rimuovi annotazioni",
"desc": "Rimuove tutti i commenti/annotazioni da un PDF",
"tags": "elimina,pulisci,rimuovi"
"desc": "Rimuove tutti i commenti/annotazioni da un PDF"
},
"compare": {
"title": "Compara",
"desc": "Vedi e compara le differenze tra due PDF.",
"tags": "differenza"
"desc": "Vedi e compara le differenze tra due PDF."
},
"removeCertSign": {
"title": "Rimuovere firma dal certificato",
"desc": "Rimuovi la firma del certificato dal PDF",
"tags": "rimuovi,elimina,sblocca"
"desc": "Rimuovi la firma del certificato dal PDF"
},
"pageLayout": {
"title": "Layout multipagina",
"desc": "Unisci più pagine di un documento PDF in un'unica pagina",
"tags": "layout,disponi,combina"
"desc": "Unisci più pagine di un documento PDF in un'unica pagina"
},
"bookletImposition": {
"title": "Imposizione a libretto",
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura",
"tags": "opuscolo,stampa,rilegatura"
"desc": "Crea libretti con corretto ordinamento pagine e layout multipagina per stampa e rilegatura"
},
"scalePages": {
"title": "Regola le dimensioni/scala della pagina",
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti.",
"tags": "ridimensiona,adatta,scala"
"desc": "Modificare le dimensioni/scala della pagina e/o dei suoi contenuti."
},
"addPageNumbers": {
"title": "Aggiungi numeri di pagina",
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita",
"tags": "numero,paginazione,conteggio"
"desc": "Aggiungi numeri di pagina in tutto un documento in una posizione prestabilita"
},
"autoRename": {
"title": "Rinomina automatica file PDF",
"desc": "Rinomina automaticamente un file PDF in base allintestazione rilevata",
"tags": "auto-rilevamento,basato su intestazione,organizza,rinomina"
"desc": "Rinomina automaticamente un file PDF in base allintestazione rilevata"
},
"adjustContrast": {
"title": "Regola colori/contrasto",
"desc": "Regola contrasto, saturazione e luminosità di un PDF",
"tags": "contrasto,luminosità,saturazione"
"desc": "Regola contrasto, saturazione e luminosità di un PDF"
},
"crop": {
"title": "Ritaglia PDF",
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)",
"tags": "ritaglia,taglia,ridimensiona"
"desc": "Ritaglia un PDF per ridurne le dimensioni (mantiene il testo!)"
},
"autoSplitPDF": {
"title": "Pagine divise automaticamente",
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato",
"tags": "auto,dividi,QR"
"desc": "Dividi automaticamente il PDF scansionato con il codice QR dello divisore di pagina fisico scansionato"
},
"sanitize": {
"title": "Sanitizza",
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF",
"tags": "pulisci,elimina,rimuovi"
"desc": "Rimuovi elementi potenzialmente dannosi dai PDF"
},
"getPdfInfo": {
"title": "Ottieni TUTTE le informazioni in PDF",
"desc": "Raccogli tutte le informazioni possibili sui PDF",
"tags": "info,metadati,dettagli"
"desc": "Raccogli tutte le informazioni possibili sui PDF"
},
"pdfToSinglePage": {
"title": "PDF in un'unica pagina di grandi dimensioni",
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina",
"tags": "combina,unisci,singola"
"desc": "Unisce tutte le pagine PDF in un'unica grande pagina"
},
"showJS": {
"title": "Mostra Javascript",
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF",
"tags": "javascript,codice,script"
"desc": "Cerca e visualizza qualsiasi JS inserito in un PDF"
},
"redact": {
"title": "Redazione manuale",
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)",
"tags": "censura,oscura,nascondi"
"desc": "Redige un PDF in base al testo selezionato, alle forme disegnate e/o alle pagina selezionata(e)"
},
"overlayPdfs": {
"title": "Sovrapponi PDF",
"desc": "Sovrapponi PDF sopra un altro PDF",
"tags": "sovrapponi,combina,impila"
"desc": "Sovrapponi PDF sopra un altro PDF"
},
"splitBySections": {
"title": "Dividi PDF per sezioni",
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole",
"tags": "dividi,sezioni,separa"
"desc": "Divide ogni pagina di un PDF in sezioni orizzontali e verticali più piccole"
},
"addStamp": {
"title": "Aggiungi timbro al PDF",
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche",
"tags": "timbro,marca,sigillo"
"desc": "Aggiungi timbri di testo o immagine in posizioni specifiche"
},
"removeImage": {
"title": "Rimuovi immagine",
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file",
"tags": "rimuovi,elimina,pulisci"
"desc": "Rimuovi le immagini dal PDF per ridurre la dimensione del file"
},
"splitByChapters": {
"title": "Dividi PDF per capitoli",
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli.",
"tags": "dividi,capitoli,struttura"
"desc": "Dividi un PDF in più file in base alla struttura dei capitoli."
},
"validateSignature": {
"title": "Convalida la firma PDF",
"desc": "Verificare le firme digitali e i certificati nei documenti PDF",
"tags": "convalida,verifica,certificato"
"desc": "Verificare le firme digitali e i certificati nei documenti PDF"
},
"swagger": {
"title": "Documentazione API",
"desc": "Visualizza documentazione API e testa gli endpoint",
"tags": "API,documentazione,test"
"desc": "Visualizza documentazione API e testa gli endpoint"
},
"fakeScan": {
"title": "Finta scansione",
@@ -585,38 +538,31 @@
},
"editTableOfContents": {
"title": "Modifica indice",
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF",
"tags": "segnalibri,contenuti,modifica"
"desc": "Aggiungi o modifica segnalibri e sommario nei documenti PDF"
},
"manageCertificates": {
"title": "Gestisci certificati",
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF.",
"tags": "certificati,importa,esporta"
"desc": "Importa, esporta o elimina i file certificato usati per firmare i PDF."
},
"read": {
"title": "Leggi",
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione.",
"tags": "visualizza,apri,mostra"
"desc": "Visualizza e annota PDF. Evidenzia testo, disegna o inserisci commenti per revisione e collaborazione."
},
"reorganizePages": {
"title": "Riorganizza pagine",
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo draganddrop.",
"tags": "riordina,riorganizza,organizza"
"desc": "Riorganizza, duplica o elimina pagine PDF con controllo visivo draganddrop."
},
"extractPages": {
"title": "Estrai pagine",
"desc": "Estrai pagine specifiche da un PDF",
"tags": "estrai,seleziona,copia"
"desc": "Estrai pagine specifiche da un PDF"
},
"removePages": {
"title": "Rimuovi",
"desc": "Elimina alcune pagine dal PDF.",
"tags": "elimina,estrai,escludi"
"desc": "Elimina alcune pagine dal PDF."
},
"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",
"tags": "auto,dividi,dimensione"
"desc": "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti"
},
"replaceColorPdf": {
"title": "Sostituisci e inverti il colore",
@@ -624,13 +570,11 @@
},
"devApi": {
"title": "API",
"desc": "Link alla documentazione API",
"tags": "API,sviluppo,documentazione"
"desc": "Link alla documentazione API"
},
"devFolderScanning": {
"title": "Scansione cartelle automatizzata",
"desc": "Link alla guida per scansione cartelle automatizzata",
"tags": "automazione,cartella,scansione"
"desc": "Link alla guida per scansione cartelle automatizzata"
},
"devSsoGuide": {
"title": "Guida SSO",
@@ -650,17 +594,7 @@
},
"automate": {
"title": "Automatizza",
"desc": "Crea flussi multistep 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"
"desc": "Crea flussi multistep concatenando azioni PDF. Ideale per attività ricorrenti."
}
},
"landing": {
@@ -720,9 +654,7 @@
},
"error": {
"failed": "Si è verificato un errore durante lunione 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",
@@ -981,20 +913,7 @@
"10": "Unione pari-dispari",
"11": "Duplica tutte le pagine"
},
"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)."
}
"placeholder": "(ad es. 1,3,2 o 4-8,2,10-12 o 2n-1)"
},
"addImage": {
"tags": "img,jpg,immagine,foto",
@@ -1428,7 +1347,7 @@
},
"trapped": {
"label": "Stato Trapped",
"unknown": "Sconosciuto",
"unknown": "Unknown",
"true": "True",
"false": "False"
},
@@ -1619,13 +1538,7 @@
"header": "Estrai immagini",
"selectText": "Seleziona il formato in cui salvare le immagini estratte",
"allowDuplicates": "Salva le immagini duplicate",
"submit": "Estrai",
"error": {
"failed": "Si è verificato un errore durante l'estrazione delle immagini dal PDF."
},
"settings": {
"title": "Impostazioni"
}
"submit": "Estrai"
},
"pdfToPDFA": {
"tags": "archivio,a lungo termine,standard,conversione,archiviazione,conservazione",
@@ -1702,14 +1615,8 @@
"title": "Firma",
"header": "Firma PDF",
"upload": "Carica immagine",
"draw": {
"clear": "Cancella",
"title": "Disegna la tua firma"
},
"text": {
"name": "Nome firmatario",
"placeholder": "Inserisci il tuo nome completo"
},
"draw": "Disegna Firma",
"text": "Testo",
"clear": "Cancella",
"add": "Aggiungi",
"saved": "Firme salvate",
@@ -1725,35 +1632,7 @@
"previous": "Pagina precedente",
"maintainRatio": "Attiva il mantenimento delle proporzioni",
"undo": "Annulla",
"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"
}
"redo": "Rifare"
},
"flatten": {
"tags": "statico,disattivato,non interattivo,ottimizzato",
@@ -1772,8 +1651,7 @@
"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.",
"flattenOnlyForms": "Appiattisci solo i moduli"
"note": "Il flattening rimuove gli elementi interattivi dal PDF, rendendoli non modificabili."
},
"results": {
"title": "Risultati Flatten"
@@ -1869,17 +1747,7 @@
"tags": "commenti,evidenziazioni,note,markup,rimozione",
"title": "Rimuovi Annotazioni",
"header": "Rimuovi Annotazioni",
"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"
}
"submit": "Rimuovi"
},
"compare": {
"tags": "differenziare,contrastare,cambiare,analisi",
@@ -3156,13 +3024,7 @@
"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",
"removeEmbeddedFiles": "Rimuovi file incorporati",
"removeFonts": "Rimuovi caratteri",
"removeJavaScript": "Rimuovi JavaScript",
"removeLinks": "Rimuovi collegamenti",
"removeMetadata": "Rimuovi metadati documento",
"removeXMPMetadata": "Rimuovi metadati XMP"
"removeFonts.desc": "Rimuovi i font incorporati dal PDF"
}
},
"addPassword": {
@@ -3432,56 +3294,5 @@
}
},
"termsAndConditions": "Termini e condizioni",
"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"
"logOut": "Esci"
}
+82 -271
View File
@@ -87,10 +87,7 @@
"showStack": "显示堆栈跟踪",
"copyStack": "复制堆栈跟踪",
"githubSubmit": "GitHub - 提交工单",
"discordSubmit": "Discord - 提交支持帖子",
"dismissAllErrors": "关闭所有错误",
"encryptedPdfMustRemovePassword": "此 PDF 已加密或受密码保护。请在转换为 PDF/A 之前将其解锁。",
"incorrectPasswordProvided": "PDF 密码不正确或未提供。"
"discordSubmit": "Discord - 提交支持帖子"
},
"warning": {
"tooltipTitle": "警告"
@@ -361,223 +358,179 @@
"sortBy": "排序:",
"multiTool": {
"title": "PDF 多功能工具",
"desc": "合并、旋转、重新排列和删除 PDF 页面",
"tags": "多个,工具"
"desc": "合并、旋转、重新排列和删除 PDF 页面"
},
"merge": {
"title": "合并",
"desc": "轻松将多个 PDF 合并成一个。",
"tags": "组合,合并,联合"
"desc": "轻松将多个 PDF 合并成一个。"
},
"split": {
"title": "拆分",
"desc": "将 PDF 拆分为多个文档。",
"tags": "分割,分离,拆分"
"desc": "将 PDF 拆分为多个文档。"
},
"rotate": {
"title": "旋转",
"desc": "旋转 PDF。",
"tags": "旋转,翻转,定向"
"desc": "旋转 PDF。"
},
"convert": {
"title": "转换",
"desc": "在不同格式之间转换文件",
"tags": "转换,更改"
"desc": "在不同格式之间转换文件"
},
"pdfOrganiser": {
"title": "整理",
"desc": "按任意顺序删除/重新排列页面。",
"tags": "组织,重新排列,重新排序"
"desc": "按任意顺序删除/重新排列页面。"
},
"addImage": {
"title": "在 PDF 中添加图片",
"desc": "将图像添加到 PDF 的指定位置。",
"tags": "插入,嵌入,放置"
"desc": "将图像添加到 PDF 的指定位置。"
},
"addAttachments": {
"title": "添加附件",
"desc": "向 PDF 添加或移除嵌入文件(附件)",
"tags": "嵌入,附加,包含"
"desc": "向 PDF 添加或移除嵌入文件(附件)"
},
"watermark": {
"title": "添加水印",
"desc": "在 PDF 中添加自定义水印。",
"tags": "印章,标记,叠加"
"desc": "在 PDF 中添加自定义水印。"
},
"removePassword": {
"title": "删除密码",
"desc": "从 PDF 文档中移除密码保护。",
"tags": "解锁"
"desc": "从 PDF 文档中移除密码保护。"
},
"compress": {
"title": "压缩",
"desc": "压缩 PDF 文件以减小文件大小。",
"tags": "缩小,减少,优化"
"desc": "压缩 PDF 文件以减小文件大小。"
},
"unlockPDFForms": {
"title": "解锁PDF表单",
"desc": "移除表单字段只读属性",
"tags": "解锁,启用,编辑"
"desc": "移除表单字段只读属性"
},
"changeMetadata": {
"title": "更改元数据",
"desc": "更改/删除/添加 PDF 文档的元数据。",
"tags": "编辑,修改,更新"
"desc": "更改/删除/添加 PDF 文档的元数据。"
},
"ocr": {
"title": "运行 OCR /清理扫描",
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。",
"tags": "提取,扫描"
"desc": "清理和识别 PDF 中的图像文本,并将其转换为可编辑文本。"
},
"extractImages": {
"title": "提取图像",
"desc": "从 PDF 中提取所有图像并保存到压缩包中。",
"tags": "提取,保存,导出"
"desc": "从 PDF 中提取所有图像并保存到压缩包中。"
},
"scannerImageSplit": {
"title": "检测/拆分扫描照片",
"desc": "从照片/PDF 中拆分出多张照片",
"tags": "检测,拆分,照片"
"desc": "从照片/PDF 中拆分出多张照片"
},
"sign": {
"title": "签名",
"desc": "通过绘图、文字或图像向 PDF 添加签名",
"tags": "签名,亲笔签名"
"desc": "通过绘图、文字或图像向 PDF 添加签名"
},
"flatten": {
"title": "展平",
"desc": "从 PDF 中删除所有互动元素和表单",
"tags": "简化,删除,交互式"
"desc": "从 PDF 中删除所有互动元素和表单"
},
"certSign": {
"title": "使用证书签名",
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名",
"tags": "认证,PEM,P12,官方,加密,签名,证书,PKCS12,JKS,服务器,手动,自动"
"desc": "使用证书/密钥(PEM/P12)对PDF进行签名"
},
"repair": {
"title": "修复",
"desc": "尝试修复损坏/损坏的 PDF",
"tags": "修复,恢复"
"desc": "尝试修复损坏/损坏的 PDF"
},
"removeBlanks": {
"title": "删除空白页",
"desc": "检测并删除文档中的空白页",
"tags": "删除,清理,空白"
"desc": "检测并删除文档中的空白页"
},
"removeAnnotations": {
"title": "删除标注",
"desc": "删除 PDF 中的所有标注/评论",
"tags": "删除,清理,删除"
"desc": "删除 PDF 中的所有标注/评论"
},
"compare": {
"title": "比较",
"desc": "比较并显示两个 PDF 文档之间的差异",
"tags": "差异"
"desc": "比较并显示两个 PDF 文档之间的差异"
},
"removeCertSign": {
"title": "移除证书签名",
"desc": "移除 PDF 的证书签名",
"tags": "删除,删除,解锁"
"desc": "移除 PDF 的证书签名"
},
"pageLayout": {
"title": "多页布局",
"desc": "将 PDF 文档的多个页面合并成一页",
"tags": "布局,排列,组合"
"desc": "将 PDF 文档的多个页面合并成一页"
},
"bookletImposition": {
"title": "小册子拼版",
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订",
"tags": "小册子,打印,装订"
"desc": "创建具有正确页面顺序和多页布局的小册子,用于打印和装订"
},
"scalePages": {
"title": "调整页面尺寸/缩放",
"desc": "调整页面及/或其内容的尺寸/缩放",
"tags": "调整大小,调整,缩放"
"desc": "调整页面及/或其内容的尺寸/缩放"
},
"addPageNumbers": {
"title": "添加页码",
"desc": "在文档的指定位置添加页码",
"tags": "编号,分页,计数"
"desc": "在文档的指定位置添加页码"
},
"autoRename": {
"title": "自动重命名 PDF 文件",
"desc": "基于检测到的页眉自动重命名 PDF 文件",
"tags": "自动检测,基于标题,组织,重新标记"
"desc": "基于检测到的页眉自动重命名 PDF 文件"
},
"adjustContrast": {
"title": "调整颜色/对比度",
"desc": "调整 PDF 的对比度、饱和度和亮度",
"tags": "对比度,亮度,饱和度"
"desc": "调整 PDF 的对比度、饱和度和亮度"
},
"crop": {
"title": "裁剪 PDF",
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)",
"tags": "裁剪,剪切,调整大小"
"desc": "裁剪 PDF 以减小其文件大小(保留文本!)"
},
"autoSplitPDF": {
"title": "自动拆分页面",
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF",
"tags": "自动,拆分,QR"
"desc": "使用物理扫描页面分割器 QR 代码自动拆分扫描的 PDF"
},
"sanitize": {
"title": "安全清理",
"desc": "移除 PDF 文件中的潜在有害元素",
"tags": "清理,清除,删除"
"desc": "移除 PDF 文件中的潜在有害元素"
},
"getPdfInfo": {
"title": "获取 PDF 的所有信息",
"desc": "获取 PDF 的所有可能的信息",
"tags": "信息,元数据,详细信息"
"desc": "获取 PDF 的所有可能的信息"
},
"pdfToSinglePage": {
"title": "PDF 转单一大页",
"desc": "将所有 PDF 页面合并为一个大的单页",
"tags": "组合,合并,单页"
"desc": "将所有 PDF 页面合并为一个大的单页"
},
"showJS": {
"title": "显示 JavaScript",
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码",
"tags": "javascript,代码,脚本"
"desc": "搜索并显示嵌入到 PDF 中的任何 JavaScript 代码"
},
"redact": {
"title": "手动修订",
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF",
"tags": "审查,涂黑,隐藏"
"desc": "根据选定的文本、绘制的形状和/或选定的页面编辑PDF"
},
"overlayPdfs": {
"title": "叠加 PDF",
"desc": "将一个 PDF 叠加到另一个 PDF 之上",
"tags": "叠加,组合,堆叠"
"desc": "将一个 PDF 叠加到另一个 PDF 之上"
},
"splitBySections": {
"title": "按区块拆分 PDF",
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块",
"tags": "拆分,部分,分割"
"desc": "将 PDF 的每一页分割为更小的横向与纵向区块"
},
"addStamp": {
"title": "向 PDF 添加印章",
"desc": "在指定位置添加文本或图像印章",
"tags": "印章,标记,盖章"
"desc": "在指定位置添加文本或图像印章"
},
"removeImage": {
"title": "删除图像",
"desc": "删除图像减少 PDF 大小",
"tags": "删除,删除,清理"
"desc": "删除图像减少 PDF 大小"
},
"splitByChapters": {
"title": "按章节拆分 PDF",
"desc": "根据其章节结构将 PDF 拆分为多个文件。",
"tags": "拆分,章节,结构"
"desc": "根据其章节结构将 PDF 拆分为多个文件。"
},
"validateSignature": {
"title": "验证 PDF 签名",
"desc": "验证 PDF 文档中的数字签名和证书",
"tags": "验证,核实,证书"
"desc": "验证 PDF 文档中的数字签名和证书"
},
"swagger": {
"title": "API 文档",
"desc": "查看 API 文档并测试端点",
"tags": "API,文档,测试"
"desc": "查看 API 文档并测试端点"
},
"fakeScan": {
"title": "伪扫描",
@@ -585,38 +538,31 @@
},
"editTableOfContents": {
"title": "编辑目录",
"desc": "为 PDF 文档添加或编辑目录和书签",
"tags": "书签,目录,编辑"
"desc": "为 PDF 文档添加或编辑目录和书签"
},
"manageCertificates": {
"title": "管理证书",
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。",
"tags": "证书,导入,导出"
"desc": "导入、导出或删除用于签名 PDF 的数字证书文件。"
},
"read": {
"title": "阅读",
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。",
"tags": "查看,打开,显示"
"desc": "查看与批注 PDF。高亮、绘制或插入评论以便审阅协作。"
},
"reorganizePages": {
"title": "重组页面",
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。",
"tags": "重新排列,重新排序,组织"
"desc": "通过可视化拖放控制重新排列、复制或删除 PDF 页面。"
},
"extractPages": {
"title": "提取页面",
"desc": "从 PDF 文档中提取特定页面",
"tags": "提取,选择,复制"
"desc": "从 PDF 文档中提取特定页面"
},
"removePages": {
"title": "删除",
"desc": "从 PDF 文档中删除不需要的页面。",
"tags": "删除,提取,排除"
"desc": "从 PDF 文档中删除不需要的页面。"
},
"autoSizeSplitPDF": {
"title": "自动根据大小/数目拆分 PDF",
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数",
"tags": "自动,拆分,大小"
"desc": "将单个 PDF 拆分为多个文档,基于大小、页数或文档数"
},
"replaceColorPdf": {
"title": "替换和反转颜色",
@@ -624,13 +570,11 @@
},
"devApi": {
"title": "API",
"desc": "跳转至 API 文档",
"tags": "API,开发,文档"
"desc": "跳转至 API 文档"
},
"devFolderScanning": {
"title": "自动文件夹扫描",
"desc": "跳转至自动文件夹扫描指南",
"tags": "自动化,文件夹,扫描"
"desc": "跳转至自动文件夹扫描指南"
},
"devSsoGuide": {
"title": "SSO 指南",
@@ -650,17 +594,7 @@
},
"automate": {
"title": "自动化",
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。",
"tags": "工作流,序列,自动化"
},
"replaceColor": {
"desc": "替换或反转 PDF 文档中的颜色",
"title": "替换和反转颜色"
},
"scannerEffect": {
"desc": "创建看起来像扫描的 PDF",
"tags": "扫描,模拟,创建",
"title": "扫描仪效果"
"desc": "通过串联 PDF 操作构建多步工作流。适合重复性任务。"
}
},
"landing": {
@@ -720,9 +654,7 @@
},
"error": {
"failed": "合并 PDF 时发生错误。"
},
"generateTableOfContents": "在合并的文件中生成目录?",
"removeDigitalSignature": "在合并的文件中删除数字签名?"
}
},
"split": {
"tags": "页面操作,划分,多页面,剪切,服务器端",
@@ -883,7 +815,7 @@
"settings": "设置",
"conversionCompleted": "转换完成",
"results": "结果",
"defaultFilename": "已转换文件",
"defaultFilename": "converted_file",
"conversionResults": "转换结果",
"convertFrom": "转换来源",
"convertTo": "转换为",
@@ -981,20 +913,7 @@
"10": "奇偶合并",
"11": "复制所有页面"
},
"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": "排列页面以进行侧缝小册子打印(针对侧面装订进行了优化)。"
}
"placeholder": "(例如:1,3,2 或 4-8,2,10-12 或 2n-1"
},
"addImage": {
"tags": "图像、JPG、图片、照片",
@@ -1018,7 +937,7 @@
"desc": "向 PDF 添加文本或图像水印",
"completed": "已添加水印",
"submit": "添加水印",
"filenamePrefix": "已加水印",
"filenamePrefix": "watermarked",
"error": {
"failed": "向 PDF 添加水印时发生错误。"
},
@@ -1217,7 +1136,7 @@
"placeholder": "例如:1,3,5-8,10",
"error": "无效的页码格式。使用数字、范围(1-5)或数学表达式(2n+1)"
},
"filenamePrefix": "已删除页面",
"filenamePrefix": "pages_removed",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -1365,7 +1284,7 @@
"header": "解锁 PDF 表单",
"submit": "Remove",
"description": "该工具将移除 PDF 表单字段的只读限制,使其可编辑、可填写。",
"filenamePrefix": "已解锁表单",
"filenamePrefix": "unlocked_forms",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -1380,7 +1299,7 @@
"tags": "标题、作者、日期、创建、时间、发布者、制作人、统计数据",
"header": "更改元数据",
"submit": "更改",
"filenamePrefix": "元数据",
"filenamePrefix": "metadata",
"settings": {
"title": "元数据设置"
},
@@ -1428,7 +1347,7 @@
},
"trapped": {
"label": "陷印状态",
"unknown": "未知",
"unknown": "Unknown",
"true": "True",
"false": "False"
},
@@ -1619,13 +1538,7 @@
"header": "提取图像",
"selectText": "选择图像格式,将提取的图像转换为",
"allowDuplicates": "保存重复图像",
"submit": "提取",
"error": {
"failed": "从 PDF 提取图像时发生错误。"
},
"settings": {
"title": "设置"
}
"submit": "提取"
},
"pdfToPDFA": {
"tags": "归档、长期、标准、转换、存储、保存",
@@ -1702,14 +1615,8 @@
"title": "签名",
"header": "签署 PDF",
"upload": "上传图片",
"draw": {
"clear": "清除",
"title": "绘制您的签名"
},
"text": {
"name": "签署人姓名",
"placeholder": "输入您的全名"
},
"draw": "绘制签名",
"text": "文本输入",
"clear": "清除",
"add": "添加",
"saved": "已保存签名",
@@ -1725,35 +1632,7 @@
"previous": "上一页",
"maintainRatio": "切换保持长宽比",
"undo": "撤销",
"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": "签名类型"
}
"redo": "重做"
},
"flatten": {
"tags": "静态、停用、非交互、简化",
@@ -1761,7 +1640,7 @@
"header": "展平 PDF",
"flattenOnlyForms": "仅展平表格",
"submit": "展平",
"filenamePrefix": "已扁平化",
"filenamePrefix": "flattened",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -1772,8 +1651,7 @@
"stepTitle": "扁平化选项",
"title": "扁平化选项",
"flattenOnlyForms.desc": "仅扁平化表单字段,保留其他交互元素",
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。",
"flattenOnlyForms": "仅扁平化表单"
"note": "扁平化会移除 PDF 的交互元素,使其不可编辑。"
},
"results": {
"title": "扁平化结果"
@@ -1809,7 +1687,7 @@
"header": "修复 PDF",
"submit": "修复",
"description": "该工具将尝试修复损坏或受损的 PDF 文件。无需额外设置。",
"filenamePrefix": "已修复",
"filenamePrefix": "repaired",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -1869,17 +1747,7 @@
"tags": "评论、高亮、笔记、标注、删除",
"title": "删除标注",
"header": "删除标注",
"submit": "删除",
"error": {
"failed": "从 PDF 删除注释时发生错误。"
},
"info": {
"description": "此工具将从您的 PDF 文档中删除所有注释(评论、高亮、笔记等)。",
"title": "关于删除注释"
},
"settings": {
"title": "设置"
}
"submit": "删除"
},
"compare": {
"tags": "区分、对比、更改、分析",
@@ -1911,7 +1779,7 @@
"certSign": {
"tags": "身份验证、PEM、P12、官方、加密",
"title": "证书签名",
"filenamePrefix": "已签名",
"filenamePrefix": "signed",
"signMode": {
"stepTitle": "签名模式",
"tooltip": {
@@ -2035,7 +1903,7 @@
"selectPDF": "选择 PDF 文件:",
"submit": "移除签名",
"description": "该工具将从您的 PDF 文档中移除数字证书签名。",
"filenamePrefix": "未签名",
"filenamePrefix": "unsigned",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -2055,7 +1923,7 @@
"submit": "提交"
},
"bookletImposition": {
"tags": "小册子,拼版,打印,装订,折叠,签名",
"tags": "booklet,imposition,printing,binding,folding,signature",
"title": "小册子拼版",
"header": "小册子拼版",
"submit": "创建小册子",
@@ -2156,7 +2024,7 @@
"submit": "提交"
},
"adjustPageScale": {
"tags": "调整大小,修改,尺寸,适应",
"tags": "resize,modify,dimension,adapt",
"title": "调整页面比例",
"header": "调整页面比例",
"scaleFactor": {
@@ -2679,7 +2547,7 @@
"header": "将 PDF 转换为单页",
"submit": "转为单页",
"description": "该工具会将 PDF 的所有页面合并为一张超长单页。宽度保持与原页面相同,高度为所有页面高度之和。",
"filenamePrefix": "单页",
"filenamePrefix": "single_page",
"files": {
"placeholder": "在主视图中选择一个 PDF 文件以开始"
},
@@ -2900,7 +2768,7 @@
"title": "API 文档",
"header": "API 文档",
"desc": "查看并测试 Stirling PDF 的 API 端点",
"tags": "api,文档,swagger,端点,开发"
"tags": "api,documentation,swagger,endpoints,development"
},
"cookieBanner": {
"popUp": {
@@ -3138,7 +3006,7 @@
"completed": "安全清理成功完成",
"error.generic": "安全清理失败",
"error.failed": "安全清理 PDF 时发生错误。",
"filenamePrefix": "已清理",
"filenamePrefix": "sanitised",
"sanitizationResults": "安全清理结果",
"steps": {
"files": "文件",
@@ -3156,13 +3024,7 @@
"removeXMPMetadata.desc": "从 PDF 中移除 XMP 元数据",
"removeMetadata.desc": "移除文档信息元数据(标题、作者等)",
"removeLinks.desc": "移除外部链接与启动动作",
"removeFonts.desc": "从 PDF 中移除嵌入字体",
"removeEmbeddedFiles": "删除嵌入文件",
"removeFonts": "删除字体",
"removeJavaScript": "删除 JavaScript",
"removeLinks": "删除链接",
"removeMetadata": "删除文档元数据",
"removeXMPMetadata": "删除 XMP 元数据"
"removeFonts.desc": "从 PDF 中移除嵌入字体"
}
},
"addPassword": {
@@ -3170,7 +3032,7 @@
"desc": "使用密码加密您的 PDF 文档。",
"completed": "已应用密码保护",
"submit": "加密",
"filenamePrefix": "已加密",
"filenamePrefix": "encrypted",
"error": {
"failed": "加密 PDF 时发生错误。"
},
@@ -3279,7 +3141,7 @@
"placeholder": "输入当前密码",
"completed": "密码已配置"
},
"filenamePrefix": "已解密",
"filenamePrefix": "decrypted",
"error": {
"failed": "移除 PDF 密码时发生错误。"
},
@@ -3432,56 +3294,5 @@
}
},
"termsAndConditions": "条款与条件",
"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": "未保存的更改"
"logOut": "退出登录"
}
+7 -5
View File
@@ -1,15 +1,17 @@
#!/usr/bin/env node
const { execSync } = require('node:child_process');
const { existsSync, mkdirSync, writeFileSync, readFileSync } = require('node:fs');
const path = require('node:path');
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 { argv } = require('node:process');
import { argv } from 'node:process';
const inputIdx = argv.indexOf('--input');
const INPUT_FILE = inputIdx > -1 ? argv[inputIdx + 1] : null;
const POSTPROCESS_ONLY = !!INPUT_FILE;
// __dirname is available in CommonJS by default
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Generate 3rd party licenses for frontend dependencies
+3 -6
View File
@@ -1,4 +1,4 @@
import { Suspense } from "react";
import React, { Suspense } from "react";
import { RainbowThemeProvider } from "./components/shared/RainbowThemeProvider";
import { FileContextProvider } from "./contexts/FileContext";
import { NavigationProvider } from "./contexts/NavigationContext";
@@ -14,7 +14,6 @@ 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";
@@ -46,11 +45,9 @@ export default function App() {
<ToolWorkflowProvider>
<SidebarProvider>
<ViewerProvider>
<SignatureProvider>
<RightRailProvider>
<HomePage />
</RightRailProvider>
</SignatureProvider>
<HomePage />
</RightRailProvider>
</ViewerProvider>
</SidebarProvider>
</ToolWorkflowProvider>
+1 -134
View File
@@ -7,132 +7,6 @@
"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",
@@ -161,13 +35,6 @@
"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",
@@ -276,7 +143,7 @@
{
"moduleName": "posthog-js",
"moduleUrl": "git+https://github.com/PostHog/posthog-js.git",
"moduleVersion": "1.268.0",
"moduleVersion": "1.266.0",
"moduleLicense": "SEE LICENSE IN LICENSE https://github.com/PostHog/posthog-js/blob/main/LICENSE",
"moduleLicenseUrl": "git+https://github.com/PostHog/posthog-js.git"
},
@@ -1,91 +0,0 @@
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;
};
@@ -1,89 +0,0 @@
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>
);
};
@@ -1,67 +0,0 @@
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}
/>
);
};
@@ -1,437 +0,0 @@
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;
@@ -1,60 +0,0 @@
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>
);
};
@@ -1,55 +0,0 @@
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>
);
};
@@ -1,126 +0,0 @@
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>
);
};
@@ -1,45 +0,0 @@
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>
);
};
@@ -1,67 +0,0 @@
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>
);
};
@@ -1,57 +0,0 @@
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,6 +149,7 @@ export default function Workbench() {
<TopControls
currentView={currentView}
setCurrentView={setCurrentView}
selectedToolKey={selectedToolId}
/>
{/* Dismiss All Errors Button */}
@@ -1,7 +1,6 @@
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";
@@ -37,9 +36,6 @@ 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;
@@ -86,12 +82,6 @@ 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;
@@ -148,16 +138,17 @@ const PageEditor = ({
// DOM-first command handlers
const handleRotatePages = useCallback((pageIds: string[], rotation: number) => {
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
executeCommandWithTracking(bulkRotateCommand);
}, [executeCommandWithTracking]);
undoManagerRef.current.executeCommand(bulkRotateCommand);
}, []);
// Command factory functions for PageThumbnail
const createRotateCommand = useCallback((pageIds: string[], rotation: number) => ({
execute: () => {
const bulkRotateCommand = new BulkRotateCommand(pageIds, rotation);
executeCommandWithTracking(bulkRotateCommand);
undoManagerRef.current.executeCommand(bulkRotateCommand);
}
}), [executeCommandWithTracking]);
}), []);
const createDeleteCommand = useCallback((pageIds: string[]) => ({
execute: () => {
@@ -183,10 +174,10 @@ const PageEditor = ({
() => getPageNumbersFromIds(selectedPageIds),
closePdf
);
executeCommandWithTracking(deleteCommand);
undoManagerRef.current.executeCommand(deleteCommand);
}
}
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
}), [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
const createSplitCommand = useCallback((position: number) => ({
execute: () => {
@@ -195,9 +186,9 @@ const PageEditor = ({
() => splitPositions,
setSplitPositions
);
executeCommandWithTracking(splitCommand);
undoManagerRef.current.executeCommand(splitCommand);
}
}), [splitPositions, executeCommandWithTracking]);
}), [splitPositions]);
// Command executor for PageThumbnail
const executeCommand = useCallback((command: any) => {
@@ -241,8 +232,8 @@ const PageEditor = ({
() => selectedPageNumbers,
closePdf
);
executeCommandWithTracking(deleteCommand);
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(deleteCommand);
}, [selectedPageIds, displayDocument, splitPositions, getPageNumbersFromIds, getPageIdsFromNumbers]);
const handleDeletePage = useCallback((pageNumber: number) => {
if (!displayDocument) return;
@@ -260,8 +251,8 @@ const PageEditor = ({
() => getPageNumbersFromIds(selectedPageIds),
closePdf
);
executeCommandWithTracking(deleteCommand);
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(deleteCommand);
}, [displayDocument, splitPositions, selectedPageIds, getPageNumbersFromIds]);
const handleSplit = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return;
@@ -307,8 +298,8 @@ const PageEditor = ({
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
};
executeCommandWithTracking(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
const handleSplitAll = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return;
@@ -353,8 +344,8 @@ const PageEditor = ({
: `Add ${selectedPositions.length - existingSplitsCount} split(s)`
};
executeCommandWithTracking(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(smartSplitCommand);
}, [selectedPageIds, displayDocument, splitPositions, setSplitPositions, getPageNumbersFromIds]);
const handlePageBreak = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return;
@@ -367,8 +358,8 @@ const PageEditor = ({
() => displayDocument,
setEditedDocument
);
executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
const handlePageBreakAll = useCallback(() => {
if (!displayDocument || selectedPageIds.length === 0) return;
@@ -381,8 +372,8 @@ const PageEditor = ({
() => displayDocument,
setEditedDocument
);
executeCommandWithTracking(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(pageBreakCommand);
}, [selectedPageIds, displayDocument, getPageNumbersFromIds]);
const handleInsertFiles = useCallback(async (files: File[], insertAfterPage: number) => {
if (!displayDocument || files.length === 0) return;
@@ -425,8 +416,8 @@ const PageEditor = ({
() => displayDocument,
setEditedDocument
);
executeCommandWithTracking(reorderCommand);
}, [displayDocument, getPageNumbersFromIds, executeCommandWithTracking]);
undoManagerRef.current.executeCommand(reorderCommand);
}, [displayDocument, getPageNumbersFromIds]);
// Helper function to collect source files for multi-file export
const getSourceFiles = useCallback((): Map<FileId, File> | null => {
@@ -508,14 +499,13 @@ 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, setHasUnsavedChanges]);
}, [displayDocument, selectedPageIds, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
const onExportAll = useCallback(async () => {
if (!displayDocument) return;
@@ -562,7 +552,6 @@ 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();
@@ -581,7 +570,6 @@ const PageEditor = ({
);
pdfExportService.downloadFile(result.blob, result.filename);
setHasUnsavedChanges(false); // Clear unsaved changes after successful export
}
setExportLoading(false);
@@ -589,7 +577,7 @@ const PageEditor = ({
console.error('Export failed:', error);
setExportLoading(false);
}
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename, setHasUnsavedChanges]);
}, [displayDocument, mergedPdfDocument, splitPositions, getSourceFiles, getExportFilename]);
// Apply DOM changes to document state using dedicated service
const applyChanges = useCallback(() => {
@@ -791,14 +779,7 @@ const PageEditor = ({
)}
<NavigationWarningModal
onApplyAndContinue={async () => {
applyChanges();
}}
onExportAndContinue={async () => {
await onExportAll();
}}
/>
<NavigationWarningModal />
</Box>
);
};
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import {
Modal,
Text,
@@ -1,4 +1,4 @@
import { useRef } from "react";
import React, { useRef } from "react";
import { FileButton, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
@@ -1,4 +1,5 @@
import { Flex } from '@mantine/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useCookieConsent } from '../../hooks/useCookieConsent';
@@ -1,3 +1,4 @@
import React from "react";
import { Box, Group, Text, Button } from "@mantine/core";
import { useTranslation } from "react-i18next";
@@ -36,7 +37,7 @@ const MultiSelectControls = ({
>
{t("fileManager.clearSelection", "Clear Selection")}
</Button>
{onAddToUpload && (
<Button
size="xs"
@@ -46,7 +47,7 @@ const MultiSelectControls = ({
{t("fileManager.addToUpload", "Add to Upload")}
</Button>
)}
{onOpenInFileEditor && (
<Button
size="xs"
@@ -57,7 +58,7 @@ const MultiSelectControls = ({
{t("fileManager.openInFileEditor", "Open in File Editor")}
</Button>
)}
{onOpenInPageEditor && (
<Button
size="xs"
@@ -68,7 +69,7 @@ const MultiSelectControls = ({
{t("fileManager.openInPageEditor", "Open in Page Editor")}
</Button>
)}
{onDeleteAll && (
<Button
size="xs"
@@ -84,4 +85,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,8 +11,6 @@ const NavigationWarningModal = ({
onApplyAndContinue,
onExportAndContinue
}: NavigationWarningModalProps) => {
const { t } = useTranslation();
const {
showNavigationWarning,
hasUnsavedChanges,
@@ -30,7 +28,7 @@ const NavigationWarningModal = ({
confirmNavigation();
};
const _handleApplyAndContinue = async () => {
const handleApplyAndContinue = async () => {
if (onApplyAndContinue) {
await onApplyAndContinue();
}
@@ -54,59 +52,55 @@ const NavigationWarningModal = ({
<Modal
opened={showNavigationWarning}
onClose={handleKeepWorking}
title={t("unsavedChangesTitle", "Unsaved Changes")}
title="Unsaved Changes"
centered
size="lg"
closeOnClickOutside={false}
closeOnEscape={false}
>
<Stack gap="md">
<Text>
{t("unsavedChanges", "You have unsaved changes to your PDF. What would you like to do?")}
You have unsaved changes to your PDF. What would you like to do?
</Text>
<Group justify="space-between" gap="sm">
<Group justify="flex-end" gap="sm">
<Button
variant="light"
color="gray"
onClick={handleKeepWorking}
>
Keep Working
</Button>
<Button
variant="light"
color="red"
onClick={handleDiscardChanges}
>
{t("discardChanges", "Discard Changes")}
Discard Changes
</Button>
<Group gap="sm">
{onApplyAndContinue && (
<Button
variant="light"
color="var(--mantine-color-gray-8)"
onClick={handleKeepWorking}
color="blue"
onClick={handleApplyAndContinue}
>
{t("keepWorking", "Keep Working")}
Apply & 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>
)}
{onExportAndContinue && (
<Button
color="green"
onClick={handleExportAndContinue}
>
Export & Continue
</Button>
)}
</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,6 +249,4 @@ const QuickAccessBar = forwardRef<HTMLDivElement>((_, ref) => {
);
});
QuickAccessBar.displayName = 'QuickAccessBar';
export default QuickAccessBar;
@@ -1,4 +1,4 @@
import { createContext, useContext, ReactNode } from 'react';
import React, { createContext, useContext, ReactNode } from 'react';
import { MantineProvider } from '@mantine/core';
import { useRainbowTheme } from '../../hooks/useRainbowTheme';
import { mantineTheme } from '../../theme/mantineTheme';
+4 -22
View File
@@ -14,7 +14,6 @@ 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';
@@ -44,9 +43,6 @@ 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();
@@ -70,9 +66,6 @@ export default function RightRail() {
const { totalItems, selectedCount } = getSelectionState();
// Get export state for viewer mode
const exportState = viewerContext?.getExportState?.();
const handleSelectAll = useCallback(() => {
if (currentView === 'fileEditor' || currentView === 'viewer') {
// Select all file IDs
@@ -102,17 +95,8 @@ export default function RightRail() {
}
}, [currentView, setSelectedFiles, pageEditorFunctions]);
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') {
const handleExportAll = useCallback(() => {
if (currentView === 'fileEditor' || currentView === 'viewer') {
// Download selected files (or all if none selected)
const filesToDownload = selectedFiles.length > 0 ? selectedFiles : activeFiles;
@@ -129,7 +113,7 @@ export default function RightRail() {
// Export all pages (not just selected)
pageEditorFunctions?.onExportAll?.();
}
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions, viewerContext, signaturesApplied, selectors, fileActions]);
}, [currentView, activeFiles, selectedFiles, pageEditorFunctions]);
const handleCloseSelected = useCallback(() => {
if (currentView !== 'fileEditor') return;
@@ -461,9 +445,7 @@ export default function RightRail() {
radius="md"
className="right-rail-icon"
onClick={handleExportAll}
disabled={
currentView === 'viewer' ? !exportState?.canExport : totalItems === 0
}
disabled={currentView === 'viewer' || totalItems === 0}
>
<LocalIcon icon="download" width="1.5rem" height="1.5rem" />
</ActionIcon>
@@ -107,5 +107,3 @@ export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(({
</div>
);
});
TextInput.displayName = 'TextInput';
+1 -1
View File
@@ -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) => {
const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null, isToolSelected: boolean) => {
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,
pageEditorOption,
...(isToolSelected ? [] : [pageEditorOption]),
fileEditorOption,
];
};
@@ -83,15 +83,19 @@ const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchTyp
interface TopControlsProps {
currentView: WorkbenchType;
setCurrentView: (view: WorkbenchType) => void;
selectedToolKey?: string | null;
}
const TopControls = ({
currentView,
setCurrentView,
}: TopControlsProps) => {
selectedToolKey,
}: TopControlsProps) => {
const { isRainbowMode } = useRainbowThemeContext();
const [switchingTo, setSwitchingTo] = useState<WorkbenchType | null>(null);
const isToolSelected = selectedToolKey !== null;
const handleViewChange = useCallback((view: string) => {
if (!isValidWorkbench(view)) {
return;
@@ -118,7 +122,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)}
data={createViewOptions(currentView, switchingTo, isToolSelected)}
value={currentView}
onChange={handleViewChange}
color="blue"
@@ -33,11 +33,8 @@ 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 === 'multiTool')
selectedToolKey && leftPanelView === 'toolContent' && !NAV_IDS.includes(selectedToolKey)
);
// Local animation and hover state
@@ -50,7 +47,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) {
@@ -81,7 +78,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
setReplayAnim(false);
animTimeoutRef.current = null;
}, 500);
};
}
const firstShow = () => {
clearTimers();
@@ -91,7 +88,7 @@ const ActiveToolButton: React.FC<ActiveToolButtonProps> = ({ setActiveButton })
animTimeoutRef.current = window.setTimeout(() => {
animTimeoutRef.current = null;
}, 500);
};
}
const triggerCollapse = () => {
clearTimers();
@@ -101,7 +98,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' | 'hidden'
leftPanelView?: 'toolPicker' | 'toolContent'
): 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' | 'hidden'
leftPanelView?: 'toolPicker' | 'toolContent'
) => {
const isActive = isNavButtonActive(
config,
@@ -1,3 +1,4 @@
import React from 'react';
import { useToast } from './ToastContext';
import { ToastInstance, ToastLocation } from './types';
import { LocalIcon } from '../shared/LocalIcon';
@@ -65,7 +66,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>
@@ -73,7 +74,7 @@ export default function ToastRenderer() {
<span className="toast-count-badge">{t.count}</span>
)}
</div>
{/* Controls */}
<div className="toast-controls">
{t.expandable && (
@@ -100,20 +101,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>
);
)
}
+6 -9
View File
@@ -7,8 +7,6 @@ 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
@@ -16,7 +14,6 @@ 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
@@ -36,17 +33,17 @@ export default function ToolPanel() {
<div
ref={toolPanelRef}
data-sidebar="tool-panel"
className={`flex flex-col overflow-hidden bg-[var(--bg-toolbar)] border-r border-[var(--border-subtle)] transition-all duration-300 ease-out ${
className={`h-screen 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: isMobile ? '100%' : isPanelVisible ? '18.5rem' : '0',
width: isPanelVisible ? '18.5rem' : '0',
padding: '0'
}}
>
<div
style={{
opacity: isMobile || isPanelVisible ? 1 : 0,
opacity: isPanelVisible ? 1 : 0,
transition: 'opacity 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
height: '100%',
display: 'flex',
@@ -74,7 +71,7 @@ export default function ToolPanel() {
<div className="flex-1 flex flex-col overflow-y-auto">
<SearchResults
filteredTools={filteredTools}
onSelect={(id) => handleToolSelect(id as ToolId)}
onSelect={handleToolSelect}
searchQuery={searchQuery}
/>
</div>
@@ -83,7 +80,7 @@ export default function ToolPanel() {
<div className="flex-1 flex flex-col overflow-auto">
<ToolPicker
selectedToolKey={selectedToolKey}
onSelect={(id) => handleToolSelect(id as ToolId)}
onSelect={handleToolSelect}
filteredTools={filteredTools}
isSearching={Boolean(searchQuery && searchQuery.trim().length > 0)}
/>
@@ -1,4 +1,4 @@
import { Suspense } from "react";
import React, { 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}
@@ -1,101 +0,0 @@
/* 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;
}
@@ -1,241 +0,0 @@
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>
);
}
@@ -1,37 +0,0 @@
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.')
),
});
};
@@ -1,34 +0,0 @@
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,11 +1,12 @@
/**
* 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";
@@ -66,4 +67,4 @@ const AddWatermarkSingleStepSettings = ({ parameters, onParameterChange, disable
);
};
export default AddWatermarkSingleStepSettings;
export default AddWatermarkSingleStepSettings;
@@ -1,3 +1,4 @@
import React from "react";
import { Stack, Checkbox, Group } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -79,4 +80,4 @@ const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }
);
};
export default WatermarkFormatting;
export default WatermarkFormatting;
@@ -1,3 +1,4 @@
import React from "react";
import { Stack } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -1,3 +1,4 @@
import React from "react";
import { Stack, Text, NumberInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -59,4 +60,4 @@ const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = fals
);
};
export default WatermarkStyleSettings;
export default WatermarkStyleSettings;
@@ -1,3 +1,4 @@
import React from "react";
import { Stack, Text, Select, ColorInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters";
@@ -26,7 +27,7 @@ const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }:
format="hex"
/>
</Stack>
<Stack gap="xs">
<Text size="xs" fw={500}>
{t("watermark.settings.alphabet", "Alphabet")}
@@ -1,3 +1,4 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { Title, Stack, Divider } from "@mantine/core";
import AddCircleOutline from "@mui/icons-material/AddCircleOutline";
@@ -18,11 +19,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 { useState } from "react";
import React, { 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 { useState } from "react";
import React, { 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,3 +1,4 @@
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,3 +1,4 @@
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,3 +1,4 @@
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 { useMemo } from "react";
import React, { 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,3 +1,4 @@
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,3 +1,4 @@
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 { useState, useMemo } from "react";
import React, { 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;
@@ -1,46 +0,0 @@
import { useTranslation } from 'react-i18next';
import { Stack, Select, Checkbox } from '@mantine/core';
import { ExtractImagesParameters } from '../../../hooks/tools/extractImages/useExtractImagesParameters';
interface ExtractImagesSettingsProps {
parameters: ExtractImagesParameters;
onParameterChange: <K extends keyof ExtractImagesParameters>(key: K, value: ExtractImagesParameters[K]) => void;
disabled?: boolean;
}
const ExtractImagesSettings = ({
parameters,
onParameterChange,
disabled = false
}: ExtractImagesSettingsProps) => {
const { t } = useTranslation();
return (
<Stack gap="md">
<Select
label={t('extractImages.selectText', 'Output Format')}
value={parameters.format}
onChange={(value) => {
const allowedFormats = ['png', 'jpg', 'gif'] as const;
const format = allowedFormats.includes(value as any) ? (value as typeof allowedFormats[number]) : 'png';
onParameterChange('format', format);
}}
data={[
{ value: 'png', label: 'PNG' },
{ value: 'jpg', label: 'JPG' },
{ value: 'gif', label: 'GIF' },
]}
disabled={disabled}
/>
<Checkbox
label={t('extractImages.allowDuplicates', 'Allow Duplicate Images')}
checked={parameters.allowDuplicates}
onChange={(event) => onParameterChange('allowDuplicates', event.currentTarget.checked)}
disabled={disabled}
/>
</Stack>
);
};
export default ExtractImagesSettings;
@@ -1,61 +0,0 @@
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>
);
}
@@ -1,37 +0,0 @@
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).')
},
];
@@ -1,26 +0,0 @@
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,64 +0,0 @@
import { Divider, Select, Stack, TextInput } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { ReorganizePagesParameters } from '../../../hooks/tools/reorganizePages/useReorganizePagesParameters';
import { getReorganizePagesModeData } from './constants';
export default function ReorganizePagesSettings({
parameters,
onParameterChange,
disabled,
}: {
parameters: ReorganizePagesParameters;
onParameterChange: <K extends keyof ReorganizePagesParameters>(
key: K,
value: ReorganizePagesParameters[K]
) => void;
disabled?: boolean;
}) {
const { t } = useTranslation();
const modeData = getReorganizePagesModeData(t);
const requiresOrder = parameters.customMode === '' || parameters.customMode === 'DUPLICATE';
const selectedMode = modeData.find(mode => mode.value === parameters.customMode) || modeData[0];
return (
<Stack gap="sm">
<Select
label={t('pdfOrganiser.mode._value', 'Organization mode')}
data={modeData}
value={parameters.customMode}
onChange={(v) => onParameterChange('customMode', v ?? '')}
disabled={disabled}
/>
{selectedMode && (
<div
style={{
backgroundColor: 'var(--information-text-bg)',
color: 'var(--information-text-color)',
padding: '8px 12px',
borderRadius: '8px',
marginTop: '4px',
fontSize: '0.75rem',
textAlign: 'center'
}}
>
{selectedMode.description}
</div>
)}
{requiresOrder && (
<>
<Divider/>
<TextInput
label={t('pageOrderPrompt', 'Page order / ranges')}
placeholder={t('pdfOrganiser.placeholder', 'e.g. 1,3,2,4-6')}
value={parameters.pageNumbers}
onChange={(e) => onParameterChange('pageNumbers', e.currentTarget.value)}
disabled={disabled}
/>
</>
)}
</Stack>
);
}
@@ -1,59 +0,0 @@
import { TFunction } from 'i18next';
export const getReorganizePagesModeData = (t: TFunction) => [
{
value: '',
label: t('pdfOrganiser.mode.1', 'Custom Page Order'),
description: t('pdfOrganiser.mode.desc.CUSTOM', 'Use a custom sequence of page numbers or expressions to define a new order.')
},
{
value: 'REVERSE_ORDER',
label: t('pdfOrganiser.mode.2', 'Reverse Order'),
description: t('pdfOrganiser.mode.desc.REVERSE_ORDER', 'Flip the document so the last page becomes first and so on.')
},
{
value: 'DUPLEX_SORT',
label: t('pdfOrganiser.mode.3', 'Duplex Sort'),
description: t('pdfOrganiser.mode.desc.DUPLEX_SORT', 'Interleave fronts then backs as if a duplex scanner scanned all fronts, then all backs (1, n, 2, n-1, …).')
},
{
value: 'BOOKLET_SORT',
label: t('pdfOrganiser.mode.4', 'Booklet Sort'),
description: t('pdfOrganiser.mode.desc.BOOKLET_SORT', 'Arrange pages for booklet printing (last, first, second, second last, …).')
},
{
value: 'SIDE_STITCH_BOOKLET_SORT',
label: t('pdfOrganiser.mode.5', 'Side Stitch Booklet Sort'),
description: t('pdfOrganiser.mode.desc.SIDE_STITCH_BOOKLET_SORT', 'Arrange pages for sidestitch booklet printing (optimized for binding on the side).')
},
{
value: 'ODD_EVEN_SPLIT',
label: t('pdfOrganiser.mode.6', 'Odd-Even Split'),
description: t('pdfOrganiser.mode.desc.ODD_EVEN_SPLIT', 'Split the document into two outputs: all odd pages and all even pages.')
},
{
value: 'ODD_EVEN_MERGE',
label: t('pdfOrganiser.mode.10', 'Odd-Even Merge'),
description: t('pdfOrganiser.mode.desc.ODD_EVEN_MERGE', 'Merge two PDFs by alternating pages: odd from the first, even from the second.')
},
{
value: 'DUPLICATE',
label: t('pdfOrganiser.mode.11', 'Duplicate all pages'),
description: t('pdfOrganiser.mode.desc.DUPLICATE', 'Duplicate each page according to the custom order count (e.g., 4 duplicates each page 4×).')
},
{
value: 'REMOVE_FIRST',
label: t('pdfOrganiser.mode.7', 'Remove First'),
description: t('pdfOrganiser.mode.desc.REMOVE_FIRST', 'Remove the first page from the document.')
},
{
value: 'REMOVE_LAST',
label: t('pdfOrganiser.mode.8', 'Remove Last'),
description: t('pdfOrganiser.mode.desc.REMOVE_LAST', 'Remove the last page from the document.')
},
{
value: 'REMOVE_FIRST_AND_LAST',
label: t('pdfOrganiser.mode.9', 'Remove First and Last'),
description: t('pdfOrganiser.mode.desc.REMOVE_FIRST_AND_LAST', 'Remove both the first and last pages from the document.')
},
];
@@ -1,107 +0,0 @@
import { Stack, Text, Select, ColorInput } from "@mantine/core";
import { useTranslation } from "react-i18next";
import { ReplaceColorParameters } from "../../../hooks/tools/replaceColor/useReplaceColorParameters";
interface ReplaceColorSettingsProps {
parameters: ReplaceColorParameters;
onParameterChange: <K extends keyof ReplaceColorParameters>(key: K, value: ReplaceColorParameters[K]) => void;
disabled?: boolean;
}
const ReplaceColorSettings = ({ parameters, onParameterChange, disabled = false }: ReplaceColorSettingsProps) => {
const { t } = useTranslation();
const replaceAndInvertOptions = [
{
value: 'HIGH_CONTRAST_COLOR',
label: t('replaceColor.options.highContrast', 'High contrast')
},
{
value: 'FULL_INVERSION',
label: t('replaceColor.options.invertAll', 'Invert all colours')
},
{
value: 'CUSTOM_COLOR',
label: t('replaceColor.options.custom', 'Custom')
}
];
const highContrastOptions = [
{
value: 'WHITE_TEXT_ON_BLACK',
label: t('replace-color.selectText.6', 'White text on black background')
},
{
value: 'BLACK_TEXT_ON_WHITE',
label: t('replace-color.selectText.7', 'Black text on white background')
},
{
value: 'YELLOW_TEXT_ON_BLACK',
label: t('replace-color.selectText.8', 'Yellow text on black background')
},
{
value: 'GREEN_TEXT_ON_BLACK',
label: t('replace-color.selectText.9', 'Green text on black background')
}
];
return (
<Stack gap="md">
<Stack gap="xs">
<Text size="sm" fw={500}>
{t('replaceColor.labels.colourOperation', 'Colour operation')}
</Text>
<Select
value={parameters.replaceAndInvertOption}
onChange={(value) => value && onParameterChange('replaceAndInvertOption', value as ReplaceColorParameters['replaceAndInvertOption'])}
data={replaceAndInvertOptions}
disabled={disabled}
/>
</Stack>
{parameters.replaceAndInvertOption === 'HIGH_CONTRAST_COLOR' && (
<Stack gap="xs">
<Text size="sm" fw={500}>
{t('replace-color.selectText.5', 'High contrast color options')}
</Text>
<Select
value={parameters.highContrastColorCombination}
onChange={(value) => value && onParameterChange('highContrastColorCombination', value as ReplaceColorParameters['highContrastColorCombination'])}
data={highContrastOptions}
disabled={disabled}
/>
</Stack>
)}
{parameters.replaceAndInvertOption === 'CUSTOM_COLOR' && (
<>
<Stack gap="xs">
<Text size="sm" fw={500}>
{t('replace-color.selectText.10', 'Choose text Color')}
</Text>
<ColorInput
value={parameters.textColor}
onChange={(value) => onParameterChange('textColor', value)}
format="hex"
disabled={disabled}
/>
</Stack>
<Stack gap="xs">
<Text size="sm" fw={500}>
{t('replace-color.selectText.11', 'Choose background Color')}
</Text>
<ColorInput
value={parameters.backGroundColor}
onChange={(value) => onParameterChange('backGroundColor', value)}
format="hex"
disabled={disabled}
/>
</Stack>
</>
)}
</Stack>
);
};
export default ReplaceColorSettings;
@@ -30,6 +30,6 @@ const ErrorNotification = ({
{error}
</Notification>
);
};
}
export default ErrorNotification;
@@ -1,3 +1,4 @@
import React from 'react';
import { Stack, Text } from '@mantine/core';
import { formatFileSize, getFileDate } from '../../../utils/fileUtils';
@@ -23,4 +24,4 @@ const FileMetadata = ({ file }: FileMetadataProps) => {
);
};
export default FileMetadata;
export default FileMetadata;
@@ -1,3 +1,4 @@
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 { useState, useEffect } from "react";
import React, { 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,3 +1,4 @@
import React from 'react';
import { Button } from '@mantine/core';
import { useTranslation } from 'react-i18next';
@@ -51,6 +52,6 @@ const OperationButton = ({
}
</Button>
);
};
}
export default OperationButton;
@@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { 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,16 +164,13 @@ export function createToolSteps() {
const isVisible = props.isVisible !== false;
const currentStepNumber = isVisible ? stepNumber++ : undefined;
const step = React.createElement(
ToolStep,
{
...props,
title,
_stepNumber: currentStepNumber,
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
},
children
);
const step = React.createElement(ToolStep, {
...props,
title,
_stepNumber: currentStepNumber,
children,
key: `step-${title.toLowerCase().replace(/\s+/g, '-')}`
});
steps.push(step);
return step;
@@ -189,9 +186,9 @@ export function createToolSteps() {
const getVisibleCount = () => {
return steps.filter(step => {
const stepProps = step.props as ToolStepProps;
const isVisible = stepProps.isVisible !== false;
const excludeFromCount = stepProps._excludeFromCount === true;
const props = step.props as ToolStepProps;
const isVisible = props.isVisible !== false;
const excludeFromCount = props._excludeFromCount === true;
return isVisible && !excludeFromCount;
}).length;
};
@@ -206,9 +203,9 @@ export function ToolStepProvider({ children, forceStepNumbers }: { children: Rea
let count = 0;
React.Children.forEach(children, (child) => {
if (React.isValidElement(child) && child.type === ToolStep) {
const stepProps = child.props as ToolStepProps;
const isVisible = stepProps.isVisible !== false;
const excludeFromCount = stepProps._excludeFromCount === true;
const props = child.props as ToolStepProps;
const isVisible = props.isVisible !== false;
const excludeFromCount = props._excludeFromCount === true;
if (isVisible && !excludeFromCount) count++;
}
});
@@ -1,86 +0,0 @@
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;
@@ -1,259 +0,0 @@
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,8 +17,7 @@ interface ToolButtonProps {
}
const ToolButton: React.FC<ToolButtonProps> = ({ id, tool, isSelected, onSelect, disableNavigation = false, matchedSynonym }) => {
// Special case: read and multiTool are navigational tools that are always available
const isUnavailable = !tool.component && !tool.link && id !== 'read' && id !== 'multiTool';
const isUnavailable = !tool.component && !tool.link;
const { getToolNavigation } = useToolNavigation();
const handleClick = (id: string) => {
@@ -1,4 +1,4 @@
import { useState, useRef, useEffect, useMemo } from "react";
import React, { useState, useRef, useEffect, useMemo } from "react";
import { Stack, Button, Text } from "@mantine/core";
import { useTranslation } from "react-i18next";
import LocalIcon from '../../shared/LocalIcon';
@@ -1,40 +0,0 @@
import { useTranslation } from 'react-i18next';
import { TooltipContent } from '../../types/tips';
export const useReplaceColorTips = (): TooltipContent => {
const { t } = useTranslation();
return {
header: {
title: t("replaceColor.tooltip.header.title", "Replace & Invert Colour Settings Overview")
},
tips: [
{
title: t("replaceColor.tooltip.description.title", "Description"),
description: t("replaceColor.tooltip.description.text", "Transform PDF colours to improve readability and accessibility. Choose from high contrast presets, invert all colours, or create custom colour schemes.")
},
{
title: t("replaceColor.tooltip.highContrast.title", "High Contrast"),
description: t("replaceColor.tooltip.highContrast.text", "Apply predefined high contrast colour combinations designed for better readability and accessibility compliance."),
bullets: [
t("replaceColor.tooltip.highContrast.bullet1", "White text on black background - Classic dark mode"),
t("replaceColor.tooltip.highContrast.bullet2", "Black text on white background - Standard high contrast"),
t("replaceColor.tooltip.highContrast.bullet3", "Yellow text on black background - High visibility option"),
t("replaceColor.tooltip.highContrast.bullet4", "Green text on black background - Alternative high contrast")
]
},
{
title: t("replaceColor.tooltip.invertAll.title", "Invert All Colours"),
description: t("replaceColor.tooltip.invertAll.text", "Completely invert all colours in the PDF, creating a negative-like effect. Useful for creating dark mode versions of documents or reducing eye strain in low-light conditions.")
},
{
title: t("replaceColor.tooltip.custom.title", "Custom Colours"),
description: t("replaceColor.tooltip.custom.text", "Define your own text and background colours using the colour pickers. Perfect for creating branded documents or specific accessibility requirements."),
bullets: [
t("replaceColor.tooltip.custom.bullet1", "Text colour - Choose the colour for text elements"),
t("replaceColor.tooltip.custom.bullet2", "Background colour - Set the background colour for the document")
]
}
]
};
};
@@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from 'react';
import React, { 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,8 +9,6 @@ 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;
@@ -35,12 +33,6 @@ 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();
@@ -186,13 +178,6 @@ 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>
</>
@@ -1,25 +0,0 @@
import { useEffect } from 'react';
import { useExportCapability } from '@embedpdf/plugin-export/react';
import { useViewer } from '../../contexts/ViewerContext';
/**
* Component that runs inside EmbedPDF context and provides export functionality
*/
export function ExportAPIBridge() {
const { provides: exportApi } = useExportCapability();
const { registerBridge } = useViewer();
useEffect(() => {
if (exportApi) {
// Register this bridge with ViewerContext
registerBridge('export', {
state: {
canExport: true,
},
api: exportApi
});
}
}, [exportApi, registerBridge]);
return null;
}
@@ -1,116 +0,0 @@
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';
@@ -17,13 +17,7 @@ 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 { 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';
@@ -35,22 +29,14 @@ 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, enableSignature = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) {
export function LocalEmbedPDF({ file, url }: 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(() => {
@@ -92,17 +78,6 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
// 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
@@ -137,11 +112,6 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
createPluginRegistration(RotatePluginPackage, {
defaultRotation: Rotation.Degree0, // Start with no rotation
}),
// Register export plugin for downloading PDFs
createPluginRegistration(ExportPluginPackage, {
defaultFileName: 'document.pdf',
}),
];
}, [pdfUrl]);
@@ -191,72 +161,7 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
minHeight: 0,
minWidth: 0
}}>
<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}
>
<EmbedPDF engine={engine} plugins={plugins}>
<ZoomAPIBridge />
<ScrollAPIBridge />
<SelectionAPIBridge />
@@ -265,9 +170,6 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
<SearchAPIBridge />
<ThumbnailAPIBridge />
<RotateAPIBridge />
{enableSignature && <SignatureAPIBridge ref={signatureApiRef} />}
{enableSignature && <HistoryAPIBridge ref={historyApiRef} />}
<ExportAPIBridge />
<GlobalPointerProvider>
<Viewport
style={{
@@ -311,17 +213,6 @@ export function LocalEmbedPDF({ file, url, enableSignature = false, onSignatureA
{/* 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>
@@ -1,315 +0,0 @@
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 { useState, useEffect } from 'react';
import React, { 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>
);
}
}
@@ -1,370 +0,0 @@
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 { useState, useEffect } from 'react';
import React, { 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]); // Remove thumbnails from dependency to prevent infinite loop
}, [visible, thumbnails]);
// Generate thumbnails when sidebar becomes visible
useEffect(() => {
+2 -1
View File
@@ -1,3 +1,4 @@
import React from 'react';
import EmbedPdfViewer from './EmbedPdfViewer';
export interface ViewerProps {
@@ -12,4 +13,4 @@ const Viewer = (props: ViewerProps) => {
return <EmbedPdfViewer {...props} />;
};
export default Viewer;
export default Viewer;
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -12,7 +12,7 @@
* Memory management handled by FileLifecycleManager (PDF.js cleanup, blob URL revocation).
*/
import { useReducer, useCallback, useEffect, useRef, useMemo } from 'react';
import React, { 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[]> => {
+4 -22
View File
@@ -109,34 +109,16 @@ export const NavigationProvider: React.FC<{
const actions: NavigationContextActions = {
setWorkbench: useCallback((workbench: WorkbenchType) => {
// 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]),
dispatch({ type: 'SET_WORKBENCH', payload: { workbench } });
}, []),
setSelectedTool: useCallback((toolId: ToolId | null) => {
dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } });
}, []),
setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => {
// 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]),
dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } });
}, []),
setHasUnsavedChanges: useCallback((hasChanges: boolean) => {
dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } });
+3 -3
View File
@@ -1,4 +1,4 @@
import { createContext, useContext, useState, useRef, useMemo } from 'react';
import React, { 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;
}
}
-178
View File
@@ -1,178 +0,0 @@
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,
};
};

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