Compare commits

...
10 Commits
Author SHA1 Message Date
Anthony Stirling b0b9eee5c8 selfisgned 2026-01-30 16:49:59 +00:00
stirlingbot[bot]GitHubstirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
0b0db1793e 🌐 Sync Translations + Update README Progress Table (#5581)
### Description of Changes

This Pull Request was automatically generated to synchronize updates to
translation files and documentation. Below are the details of the
changes made:

#### **1. Synchronization of Translation Files**
- Updated translation files
(`frontend/public/locales/*/translation.toml`) to reflect changes in the
reference file `en-GB/translation.toml`.
- Ensured consistency and synchronization across all supported language
files.
- Highlighted any missing or incomplete translations.
- **Format**: TOML

#### **2. Update README.md**
- Generated the translation progress table in `README.md` using
`counter_translation_v3.py`.
- 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

Co-authored-by: stirlingbot[bot] <195170888+stirlingbot[bot]@users.noreply.github.com>
2026-01-28 23:58:44 +00:00
Anthony StirlingandGitHub f3cf747cfe possible login fixes (#5444)
# Description of Changes

Disable TLS checks and various cert checks to allow all sorts of
selfhost machines to be connected via tauri app

Version bump

Crop tool correctly shows ghostscript as optional so its not disabled on
java only installations

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 23:57:43 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Ludy
e3982ed4c5 build(deps): bump pypdf from 6.6.0 to 6.6.2 in /testing/cucumber in the pip group across 1 directory (#5577)
Bumps the pip group with 1 update in the /testing/cucumber directory:
[pypdf](https://github.com/py-pdf/pypdf).

Updates `pypdf` from 6.6.0 to 6.6.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/releases">pypdf's
releases</a>.</em></p>
<blockquote>
<h2>Version 6.6.2, 2026-01-26</h2>
<h2>What's new</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)
by <a
href="https://github.com/stefan6419846"><code>@​stefan6419846</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.1...6.6.2">Full
Changelog</a></p>
<h2>Version 6.6.1, 2026-01-25</h2>
<h2>What's new</h2>
<h3>Robustness (ROB)</h3>
<ul>
<li><code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)
by <a
href="https://github.com/joshkersey"><code>@​joshkersey</code></a></li>
<li>Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)
by <a href="https://github.com/LudovA"><code>@​LudovA</code></a></li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.1">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/py-pdf/pypdf/blob/main/CHANGELOG.md">pypdf's
changelog</a>.</em></p>
<blockquote>
<h2>Version 6.6.2, 2026-01-26</h2>
<h3>Security (SEC)</h3>
<ul>
<li>Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.1...6.6.2">Full
Changelog</a></p>
<h2>Version 6.6.1, 2026-01-25</h2>
<h3>Robustness (ROB)</h3>
<ul>
<li><code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)</li>
<li>Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)</li>
</ul>
<p><a href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.1">Full
Changelog</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/py-pdf/pypdf/commit/ad47d50fd7475650d71913c8c0927e9a79249c75"><code>ad47d50</code></a>
REL: 6.6.2</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/b1282f8dcdc1a7b41ceab6740ffddfdf31b1fec1"><code>b1282f8</code></a>
SEC: Detect cyclic references when retrieving outlines (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3610">#3610</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/f18e13c91cd47fa740997df4a7307fbc976388e1"><code>f18e13c</code></a>
REL: 6.6.1</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/19735763b856cccf0f69630d0f582a448ec5d8bb"><code>1973576</code></a>
DEV: Bump wheel from 0.44.0 to 0.46.2 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3608">#3608</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/4740225eaa67ad2e032e63d0453ea6c80bcae158"><code>4740225</code></a>
ROB: <code>/AcroForm</code> might be NullObject (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3601">#3601</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/26fd6388754ed167e862bdd7a3eba614da191d34"><code>26fd638</code></a>
ROB: Handle missing font bounding boxes gracefully (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3600">#3600</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/affe8eddf185b2875dcfa45090b7d09f06d3ba12"><code>affe8ed</code></a>
DEV: Bump virtualenv from 20.27.0 to 20.36.1 (<a
href="https://redirect.github.com/py-pdf/pypdf/issues/3597">#3597</a>)</li>
<li><a
href="https://github.com/py-pdf/pypdf/commit/df1b91da41e38ffaf0815e3fd687c037b375c728"><code>df1b91d</code></a>
DEV: Add missing dependency to URL check</li>
<li>See full diff in <a
href="https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypdf&package-manager=pip&previous-version=6.6.0&new-version=6.6.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/Stirling-Tools/Stirling-PDF/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ludy <Ludy87@users.noreply.github.com>
2026-01-28 16:11:15 +00:00
5c7d675960 deps(embedPDF): Bump codebase to embedPDF v2.3.0 and adjust codebase for new features (#5567)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Signed-off-by: Balázs Szücs <bszucs1209@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2026-01-28 16:10:43 +00:00
Anthony StirlingandGitHub 7fc6ec5fe1 tool tags (#5568)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:36:56 +00:00
Anthony StirlingandGitHub 7722001463 xframe fix new (#5580)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:36:21 +00:00
43d4b46b31 deps(frontend, tauri): update Tauri, Rust crates, and frontend dependencies (#5569)
# Description of Changes

This pull request primarily updates dependencies for both the frontend
JavaScript and Rust (Tauri) codebases, and refactors the
`MobileUploadModal` component to consistently use a centralized API
client for backend communication. The refactor improves code
consistency, error handling, and logging in the file upload workflow.

**Dependency updates**

* Updated several Tauri-related dependencies in both
`frontend/package.json` and `frontend/src-tauri/Cargo.toml` to their
latest versions, including `@tauri-apps/api`, `@tauri-apps/plugin-fs`,
`@tauri-apps/plugin-http`, `@tauri-apps/plugin-shell`, and associated
Rust crates. This ensures better compatibility, security, and access to
new features.
[[1]](diffhunk://#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L46-R49)
[[2]](diffhunk://#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L129-R132)
[[3]](diffhunk://#diff-91e702206f8c6459b43ae72dbd6abfed8104de661dd239d13956985210f67fd0L21-R35)
* Updated `@iconify-json/material-symbols` and `@tauri-apps/cli` in
`package.json` for improved icon support and build tooling.

**Refactor: API client usage in `MobileUploadModal`**

* Replaced all direct `fetch` calls in `MobileUploadModal.tsx` with the
centralized `apiClient`, standardizing backend requests and improving
maintainability.
[[1]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aR13)
[[2]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL84-R98)
[[3]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL116-R122)
[[4]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL130-R138)
[[5]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL160-R177)
[[6]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL187-R207)
* Improved error handling, status checks, and logging throughout the
upload and session management flow, making debugging easier and the user
experience more robust.
[[1]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL84-R98)
[[2]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL130-R138)
[[3]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL148-R153)
[[4]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL160-R177)
[[5]](diffhunk://#diff-fafb4b340343062aba7b763dea5e6e13e0e330ab2ac7dfd04a2032ba79620c8aL187-R207)

**Session cleanup improvements**

* Ensured that mobile scanner sessions are reliably cleaned up both when
the modal closes and when the component unmounts, using the `apiClient`
and React's effect cleanup mechanism.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-28 10:35:59 +00:00
Anthony StirlingandGitHub eee17d4d19 pipeline fixes for naming issues (#5570)
# Description of Changes

<!--
Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)
-->

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.
2026-01-28 10:35:46 +00:00
c40144db19 chore(ci): remove automated version sync job from build workflow (#5547)
# Description of Changes

This pull request removes the `sync-versions` job from the GitHub
Actions workflow configuration. The `sync-versions` job was responsible
for ensuring that version numbers in certain frontend files matched the
version defined in `build.gradle`, and for posting a comment on pull
requests if discrepancies were found.

Workflow cleanup:

* Removed the entire `sync-versions` job from
`.github/workflows/build.yml`, including steps for syncing app versions,
checking for changes, and posting PR comments when version mismatches
were detected.

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### Translations (if applicable)

- [ ] I ran
[`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-24 23:45:48 +00:00
93 changed files with 3925 additions and 2328 deletions
-98
View File
@@ -196,104 +196,6 @@ jobs:
path: frontend/dist/
retention-days: 3
sync-versions:
if: needs.files-changed.outputs.build == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 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 JDK 21
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: "21"
distribution: "temurin"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
gradle-version: 8.14
- name: Run version sync
run: ./gradlew syncAppVersion --build-cache
- name: Check if version files have changed
id: git-check
run: |
if [[ -n $(git status --porcelain) ]]; then
echo "changes=true" >> $GITHUB_OUTPUT
else
echo "changes=false" >> $GITHUB_OUTPUT
fi
- name: Post comment on PR
if: steps.git-check.outputs.changes == 'true' && github.event_name != 'workflow_dispatch'
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.setup-bot.outputs.token }}
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const issueNumber = context.issue.number;
// Find existing comment
const comments = await github.rest.issues.listComments({
owner: repoOwner,
repo: repoName,
issue_number: issueNumber
});
const comment = comments.data.find(c => c.body.includes("## ⚠️ Version Differences"));
const commentBody = `## ⚠️ Version Differences
The versions in the following files do not match the version defined in \`build.gradle\`.
Running \`./gradlew syncAppVersion\` or \`./gradlew build\` will automatically update these files:
\`frontend/src-tauri/tauri.conf.json\`, \`frontend/src/proprietary/testing/serverExperienceSimulations.ts\`, \`frontend/src/core/testing/serverExperienceSimulations.ts\`
Please run \`./gradlew syncAppVersion\` or \`./gradlew build\`!`;
// Only update or create comments by the action user
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
if (comment && comment.user.login === expectedActor) {
// Update existing comment
await github.rest.issues.updateComment({
owner: repoOwner,
repo: repoName,
comment_id: comment.id,
body: commentBody
});
console.log("Updated existing comment.");
} else if (!comment) {
// Create new comment if no existing comment is found
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: issueNumber,
body: commentBody
});
} else {
console.log("Comment update attempt denied. Actor does not match.");
}
check-licence:
if: needs.files-changed.outputs.build == 'true'
needs: [files-changed, build]
@@ -516,6 +516,8 @@ public class EndpointConfiguration {
addEndpointAlternative("compress-pdf", "qpdf");
addEndpointAlternative("compress-pdf", "Ghostscript");
addEndpointAlternative("compress-pdf", "Java");
addEndpointAlternative("crop", "Ghostscript");
addEndpointAlternative("crop", "Java");
addEndpointAlternative("ocr-pdf", "tesseract");
addEndpointAlternative("ocr-pdf", "OCRmyPDF");
@@ -164,6 +164,7 @@ public class ApplicationProperties {
private String customGlobalAPIKey;
private Jwt jwt = new Jwt();
private Validation validation = new Validation();
private String xFrameOptions = "DENY";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
+1
View File
@@ -221,6 +221,7 @@ tasks.register('npmBuild', Exec) {
commandLine = Os.isFamily(Os.FAMILY_WINDOWS) ? ['cmd', '/c', 'npm', 'run', 'build'] : ['npm', 'run', 'build']
dependsOn npmInstall
inputs.dir(new File(frontendDir, 'src'))
inputs.dir(new File(frontendDir, 'public'))
inputs.file(new File(frontendDir, 'package.json'))
outputs.dir(frontendDistDir)
@@ -81,6 +81,7 @@ security:
revocation:
mode: none # Revocation checking mode: 'none' (disabled), 'ocsp' (OCSP only), 'crl' (CRL only), 'ocsp+crl' (OCSP with CRL fallback)
hardFail: false # Fail validation if revocation status cannot be determined (true=strict, false=soft-fail)
xFrameOptions: DENY # X-Frame-Options header value. Options: 'DENY' (default, prevents all framing), 'SAMEORIGIN' (allows framing from same domain), 'DISABLED' (no X-Frame-Options header sent). Note: automatically set to DISABLED when login is disabled
premium:
key: 00000000-0000-0000-0000-000000000000
@@ -201,6 +201,30 @@ public class SecurityConfiguration {
http.csrf(CsrfConfigurer::disable);
// Configure X-Frame-Options based on settings.yml configuration
// When login is disabled, automatically disable X-Frame-Options to allow embedding
if (!loginEnabledValue) {
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
} else {
String xFrameOption = securityProperties.getXFrameOptions();
if (xFrameOption != null) {
http.headers(
headers -> {
if ("DISABLED".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.disable());
} else if ("SAMEORIGIN".equalsIgnoreCase(xFrameOption)) {
headers.frameOptions(frameOptions -> frameOptions.sameOrigin());
} else {
// Default to DENY
headers.frameOptions(frameOptions -> frameOptions.deny());
}
});
} else {
// If not configured, use default DENY
http.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.deny()));
}
}
if (loginEnabledValue) {
http.addFilterBefore(
+2 -2
View File
@@ -67,7 +67,7 @@ springBoot {
allprojects {
group = 'stirling.software'
version = '2.4.0'
version = '2.4.1'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
@@ -537,4 +537,4 @@ tasks.register('buildRestartHelper', Jar) {
doLast {
println "restart-helper.jar created at: ${destinationDirectory.get()}/restart-helper.jar"
}
}
}
+1165 -802
View File
File diff suppressed because it is too large Load Diff
+32 -31
View File
@@ -7,27 +7,28 @@
"dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.7.7",
"@dnd-kit/core": "^6.3.1",
"@embedpdf/core": "^1.5.0",
"@embedpdf/engines": "^1.5.0",
"@embedpdf/plugin-annotation": "^1.5.0",
"@embedpdf/plugin-bookmark": "^1.5.0",
"@embedpdf/plugin-export": "^1.5.0",
"@embedpdf/plugin-history": "^1.5.0",
"@embedpdf/plugin-interaction-manager": "^1.5.0",
"@embedpdf/plugin-loader": "^1.5.0",
"@embedpdf/plugin-pan": "^1.5.0",
"@embedpdf/plugin-print": "^1.5.0",
"@embedpdf/plugin-redaction": "^1.5.0",
"@embedpdf/plugin-render": "^1.5.0",
"@embedpdf/plugin-rotate": "^1.5.0",
"@embedpdf/plugin-scroll": "^1.5.0",
"@embedpdf/plugin-search": "^1.5.0",
"@embedpdf/plugin-selection": "^1.5.0",
"@embedpdf/plugin-spread": "^1.5.0",
"@embedpdf/plugin-thumbnail": "^1.5.0",
"@embedpdf/plugin-tiling": "^1.5.0",
"@embedpdf/plugin-viewport": "^1.5.0",
"@embedpdf/plugin-zoom": "^1.5.0",
"@embedpdf/core": "^2.3.0",
"@embedpdf/engines": "^2.3.0",
"@embedpdf/models": "^2.3.0",
"@embedpdf/plugin-annotation": "^2.3.0",
"@embedpdf/plugin-bookmark": "^2.3.0",
"@embedpdf/plugin-export": "^2.3.0",
"@embedpdf/plugin-history": "^2.3.0",
"@embedpdf/plugin-document-manager": "^2.3.0",
"@embedpdf/plugin-interaction-manager": "^2.3.0",
"@embedpdf/plugin-pan": "^2.3.0",
"@embedpdf/plugin-print": "^2.3.0",
"@embedpdf/plugin-redaction": "^2.3.0",
"@embedpdf/plugin-render": "^2.3.0",
"@embedpdf/plugin-rotate": "^2.3.0",
"@embedpdf/plugin-scroll": "^2.3.0",
"@embedpdf/plugin-search": "^2.3.0",
"@embedpdf/plugin-selection": "^2.3.0",
"@embedpdf/plugin-spread": "^2.3.0",
"@embedpdf/plugin-thumbnail": "^2.3.0",
"@embedpdf/plugin-tiling": "^2.3.0",
"@embedpdf/plugin-viewport": "^2.3.0",
"@embedpdf/plugin-zoom": "^2.3.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@iconify/react": "^6.0.2",
@@ -43,13 +44,13 @@
"@supabase/supabase-js": "^2.47.13",
"@tailwindcss/postcss": "^4.1.13",
"@tanstack/react-virtual": "^3.13.12",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-fs": "^2.4.0",
"@tauri-apps/plugin-http": "^2.5.4",
"@tauri-apps/plugin-shell": "^2.3.3",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-fs": "^2.4.5",
"@tauri-apps/plugin-http": "^2.5.6",
"@tauri-apps/plugin-shell": "^2.3.4",
"autoprefixer": "^10.4.21",
"axios": "^1.12.2",
"globals": "^17.0.0",
"axios": "^1.13.2",
"globals": "^17.1.0",
"i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0",
"jszip": "^3.10.1",
@@ -58,7 +59,7 @@
"pdfjs-dist": "^5.4.149",
"peerjs": "^1.5.5",
"posthog-js": "^1.268.0",
"qrcode.react": "^4.1.0",
"qrcode.react": "^4.2.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^15.7.3",
@@ -126,10 +127,10 @@
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@iconify-json/material-symbols": "^1.2.48",
"@iconify/utils": "^3.0.2",
"@iconify-json/material-symbols": "^1.2.53",
"@iconify/utils": "^3.1.0",
"@playwright/test": "^1.55.0",
"@tauri-apps/cli": "^2.5.0",
"@tauri-apps/cli": "^2.9.6",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.8.0",
"@testing-library/react": "^16.3.0",
@@ -950,6 +950,7 @@ title = "تقسيم تلقائي بالحجم/العدد"
desc = "تقسيم ملف PDF واحد إلى مستندات متعددة بناءً على الحجم أو عدد الصفحات أو عدد المستندات"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "استبدال وعكس الألوان"
desc = "استبدال الألوان أو عكسها في مستندات PDF"
@@ -964,18 +965,22 @@ title = "المسح الآلي للمجلدات"
desc = "رابط إلى دليل المسح الآلي للمجلدات"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "دليل SSO"
desc = "رابط إلى دليل SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "إعداد معزول"
desc = "رابط إلى دليل الإعداد المعزول"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "إضافة كلمة مرور"
desc = "تشفير مستند PDF الخاص بك بكلمة مرور."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "تغيير الأذونات"
desc = "تغيير قيود المستند وأذوناته"
@@ -985,10 +990,12 @@ title = "أتمتة"
desc = "ابنِ تدفّقات عمل متعددة الخطوات بسلسلة إجراءات PDF. مثالي للمهام المتكررة."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "تراكب ملف PDF فوق آخر"
title = "تراكب ملفات PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "محرر نصوص PDF"
desc = "مراجعة وتحرير صادرات Stirling PDF بصيغة JSON مع تحرير نصوص مجمّعة وإعادة إنشاء PDF"
@@ -4526,6 +4533,13 @@ description = "الحد الأقصى لعدد محاولات تسجيل الدخ
label = "وقت إعادة التعيين لتسجيل الدخول (بالدقائق)"
description = "المدة قبل إعادة تعيين عدد المحاولات الفاشلة"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "تعطيل حماية CSRF"
description = "تعطيل حماية تزوير طلبات المواقع (غير مستحسن)"
@@ -950,6 +950,7 @@ title = "Say/Ölçüyə Əsasən Avtomatik Ayır"
desc = "PDF-i ölçüyə, səhifə sayına və ya sənəd sayına əsasən bir neçə PDF-ə ayır."
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Rəngi əvəz et və invert et"
desc = "PDF sənədlərində rəngləri əvəz edin və ya invert edin"
@@ -964,18 +965,22 @@ title = "Avtomatik qovluq skanı"
desc = "Avtomatlaşdırılmış qovluq skan təlimatına keçid"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO təlimatı"
desc = "SSO təlimatına keçid"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Şəbəkəsiz quraşdırma"
desc = "Təcrid edilmiş quraşdırma təlimatına keçid"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Şifr Əlavə Et"
desc = "Sənədini şifr ilə kilidlə."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "İcazələri Dəyişdir"
desc = "Sənəd məhdudiyyətlərini və icazələrini dəyişin"
@@ -985,10 +990,12 @@ title = "Avtomatlaşdır"
desc = "PDF əməliyyatlarını zəncirləyərək çoxaddımlı iş axınları qurun. Təkrarlanan tapşırıqlar üçün idealdır."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Bir PDF-i digərinin üstünə qoyur"
title = "Üst-Üstə Qoy"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF Mətn Redaktoru"
desc = "Qruplaşdırılmış mətn redaktəsi və PDF yenidən yaradılması ilə Stirling PDF JSON ixraclarını nəzərdən keçirin və redaktə edin"
@@ -4526,6 +4533,13 @@ description = "Hesabın bloklanmasından əvvəl maksimal uğursuz giriş cəhdl
label = "Girişi sıfırlama vaxtı (dəqiqə)"
description = "Uğursuz giriş cəhdlərinin sıfırlanmasına qədər olan vaxt"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF mühafizəsini söndür"
description = "Cross-Site Request Forgery mühafizəsini söndür (tövsiyə olunmur)"
@@ -950,6 +950,7 @@ title = "Авто делене по размер/брой"
desc = "Разделете един PDF на множество документи въз основа на размер, брой страници или брой документи"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Смени/обърни цветове"
desc = "Заместване или инвертиране на цветове в PDF документи"
@@ -964,18 +965,22 @@ title = "Авто сканиране на папки"
desc = "Връзка към ръководство за автоматизирано сканиране на папки"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Ръководство за SSO"
desc = "Връзка към SSO ръководство"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Настройка за air‑gapped среда"
desc = "Връзка към ръководство за air‑gapped настройка"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Добавете парола"
desc = "Шифровайте вашия PDF документ с парола."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Промяна на правата"
desc = "Промяна на ограниченията и разрешенията на документа"
@@ -985,10 +990,12 @@ title = "Автоматизация"
desc = "Създавайте многостъпкови работни процеси чрез свързване на PDF действия. Идеално за повтарящи се задачи."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Наслагва PDF файлове върху друг PDF"
title = "Наслагване PDF-и"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF текстов редактор"
desc = "Преглеждайте и редактирайте JSON експорти на Stirling PDF с групово редактиране на текст и повторно генериране на PDF"
@@ -4526,6 +4533,13 @@ description = "Максимален брой неуспешни опити за
label = "Време за нулиране на входа (минути)"
description = "Време, преди неуспешните опити за вход да се нулират"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Деактивирай CSRF защита"
description = "Деактивира защита срещу Cross-Site Request Forgery (не се препоръчва)"
@@ -950,6 +950,7 @@ title = "ཆེ་ཆུང་/གྲངས་ཚད་ཀྱིས་རང་
desc = "རང་འགུལ་གྱིས་ཡིག་ཆའི་ཆེ་ཆུང་དང་ཡང་ན་ཤོག་ངོས་གྲངས་ལ་གཞིགས་ནས་PDFsབགོད་དགོས།"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "ཁ་དོག་བརྗེ་ལེན་དང་བསྒྱུར་བ།"
desc = "PDFཡིག་ཆའི་ནང་ཁ་དོག་བརྗེ་ལེན་བྱེད་པའམ་ཡང་ན་བསྒྱུར་བ།"
@@ -964,18 +965,22 @@ title = "རང་འགུལ་ཅན་གྱི་སྣོད་བཅུ
desc = "རང་འགུལ་སྣོད་ཀྱི་པར་བཤེར་ལམ་སྟོན་ལ་འབྲེལ་མཐུད་བྱེད།"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO ལམ་སྟོན།"
desc = "SSO ལམ་སྟོན་ལ་འབྲེལ་མཐུད་བྱེད།"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "མཁའ་དབྱིངས་ཀྱི་ཆ་སྒྲིག་སྒྲིག་བཀོད།"
desc = "མཁའ་རླུང་གི་བར་ཐག་གིས་སྒྲིག་པའི་སྒྲིག་ལམ་ལ་འབྲེལ་མཐུད་བྱེད་པ།"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "གསང་གྲངས་ཁ་སྣོན་བྱེད།"
desc = "གསང་གྲངས་སྲུང་སྐྱོབ་དང་ཚད་བཀག་PDFཡིག་ཆ་ལ་ཁ་སྣོན་བྱེད་པ།"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "འཕོ་འགྱུར།"
desc = "ཡིག་ཆའི་ཚད་བཀག་དང་ཆོག་མཆན་བསྒྱུར་བ།"
@@ -985,10 +990,12 @@ title = "རང་འགུལ་ཅན།"
desc = "གོམ་པ་མང་པོའི་ལས་ཀའི་འགྲོ་ལུགས་དེ་PDFབྱ་སྤྱོད་མཉམ་དུ་སྒྲིག་ནས་བཟོས། ལས་འགན་ཡང་བསྐྱར་འཚོག་པར་འཚམ་པོ་ཡོད།"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "གཞན་ཞིག་གི་སྟེང་དུ་PDFགཅིག་བཀབ་དགོས།"
title = "བཀབ་པའི་PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDFཡིག་འབྲིའི་རྩོམ་སྒྲིག་པ།"
desc = "ད་ཡོད་ཀྱི་ཡིག་ཆ་དང་པར་རིས་PDFནང་དུ་རྩོམ་སྒྲིག་བྱས།"
@@ -4526,6 +4533,13 @@ description = "རྩིས་ཁྲའི་བཀག་སྡོམ་མ་
label = "ནང་འཇུག་བསྐྱར་སྒྲིག་དུས་ཚོད།"
description = "ནང་འཇུག་ཚོད་ལྟ་འཐུས་ཤོར་མ་བྱུང་གོང་གི་དུས་ཚོད།"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF སྲུང་སྐྱོབ།"
description = "ས་ཁོངས་བརྒལ་བའི་རེ་འདུན་རྫུན་བཟོ་སྲུང་སྐྱོབ།"
@@ -950,6 +950,7 @@ title = "Divideix auto per mida/pàg."
desc = "Divideix un únic PDF en múltiples documents basant-se en la mida, el nombre de pàgines o el nombre de documents"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Substitueix i inverteix el color"
desc = "Substitueix o inverteix colors en documents PDF"
@@ -964,18 +965,22 @@ title = "Escaneig automàtic de carpeta"
desc = "Enllaç a la guia d'escaneig automàtic de carpetes"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guia d'SSO"
desc = "Enllaç a la guia d'SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Configuració air-gapped"
desc = "Enllaç a la guia de configuració en entorn air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Afegir Contrasenya"
desc = "Xifra el document PDF amb contrasenya."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Canviar Permissos"
desc = "Canvia les restriccions i els permisos del document"
@@ -985,10 +990,12 @@ title = "Automatitza"
desc = "Construeix fluxos de treball multietapa enllaçant accions PDF. Ideal per a tasques recurrents."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Superposa PDFs sobre un altre PDF"
title = "Superposar PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor de text PDF"
desc = "Revisa i edita exportacions JSON de Stirling PDF amb edició de text agrupada i regeneració del PDF"
@@ -4526,6 +4533,13 @@ description = "Nombre màxim d'intents d'inici de sessió fallits abans de bloqu
label = "Temps de reinicialització d'inici de sessió (minuts)"
description = "Temps abans que es restableixin els intents d'inici de sessió fallits"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Desactiva la protecció CSRF"
description = "Desactiva la protecció contra Cross-Site Request Forgery (no recomanat)"
@@ -950,6 +950,7 @@ title = "Automaticky rozdělit podle velikosti/počtu"
desc = "Rozdělí jeden PDF na více dokumentů podle velikosti, počtu stránek nebo počtu dokumentů"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Nahradit a invertovat barvy"
desc = "Nahradit nebo invertovat barvy v dokumentech PDF"
@@ -964,18 +965,22 @@ title = "Autom. skenování složek"
desc = "Odkaz na průvodce automatizovaným skenováním složek"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Průvodce SSO"
desc = "Odkaz na průvodce SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Offline nastavení"
desc = "Odkaz na průvodce nastavením v odpojeném prostředí"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Přidat heslo"
desc = "Zašifrovat váš PDF dokument heslem."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Změnit oprávnění"
desc = "Změnit omezení a oprávnění dokumentu"
@@ -985,10 +990,12 @@ title = "Automatizace"
desc = "Vytvářejte vícekrokové workflow řetězením akcí PDF. Ideální pro opakující se úlohy."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Překryje PDF nad jiným PDF"
title = "Překrýt PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor textu PDF"
desc = "Prohlížejte a upravujte exporty JSON ze Stirling PDF se skupinovými úpravami textu a regenerací PDF"
@@ -4526,6 +4533,13 @@ description = "Maximální počet neúspěšných pokusů o přihlášení před
label = "Čas pro reset přihlášení (minuty)"
description = "Doba, po které se neúspěšné pokusy o přihlášení resetují"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Zakázat ochranu CSRF"
description = "Zakázat ochranu proti Cross-Site Request Forgery (nedoporučuje se)"
@@ -950,6 +950,7 @@ title = "Auto-opdel størrelse/antal"
desc = "Opdel en enkelt PDF i flere dokumenter baseret på størrelse, sideantal eller dokumentantal"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Erstat og inverter farver"
desc = "Erstat eller inverter farver i PDF-dokumenter"
@@ -964,18 +965,22 @@ title = "Automatiseret mappescanning"
desc = "Link til guide for automatiseret mappescanning"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO-vejledning"
desc = "Link til SSO-vejledning"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped opsætning"
desc = "Link til guide for air-gapped opsætning"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Tilføj Adgangskode"
desc = "Kryptér dit PDF-dokument med en adgangskode."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Ændre Tilladelser"
desc = "Ændr dokumentbegrænsninger og tilladelser"
@@ -985,10 +990,12 @@ title = "Automatiser"
desc = "Byg flertrins-workflows ved at kæde PDF-handlinger sammen. Ideelt til tilbagevendende opgaver."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Overlejrer PDF'er oven på en anden PDF"
title = "Overlejr PDF'er"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF-teksteditor"
desc = "Gennemse og rediger Stirling PDF JSON-eksporter med grupperet tekstredigering og regenerering af PDF"
@@ -4526,6 +4533,13 @@ description = "Maksimalt antal mislykkede loginforsøg før kontolock"
label = "Nulstil login (minutter)"
description = "Tid før mislykkede loginforsøg nulstilles"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Deaktivér CSRF-beskyttelse"
description = "Deaktivér Cross-Site Request Forgery-beskyttelse (anbefales ikke)"
@@ -950,6 +950,7 @@ title = "Teilen nach Größe/Anzahl"
desc = "Teilen Sie ein einzelnes PDF basierend auf Größe, Seitenanzahl oder Dokumentanzahl in mehrere Dokumente auf"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Farbe ersetzen & invertieren"
desc = "Farben in PDF-Dokumenten ersetzen oder invertieren"
@@ -964,18 +965,22 @@ title = "Autom. Ordner-Scan"
desc = "Link zum Leitfaden für automatisches Ordner-Scannen"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO-Anleitung"
desc = "Link zum SSO-Leitfaden"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Offline-Setup"
desc = "Link zum Air-Gap-Einrichtungsleitfaden"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Passwort hinzufügen"
desc = "Das PDF mit einem Passwort verschlüsseln"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Berechtigungen ändern"
desc = "Dokumentbeschränkungen und -berechtigungen ändern"
@@ -985,10 +990,12 @@ title = "Automatisieren"
desc = "Mehrstufige Arbeitsabläufe durch Verkettung von PDF-Aktionen erstellen. Ideal für wiederkehrende Aufgaben."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Ein PDF über ein anderes legen"
title = "PDFs überlagern"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF-Texteditor"
desc = "Vorhandenen Text und Bilder in PDFs bearbeiten"
@@ -4526,6 +4533,13 @@ description = "Maximale Anzahl fehlgeschlagener Anmeldeversuche vor Kontosperre"
label = "Zurücksetzungszeit für Anmeldungen (Minuten)"
description = "Zeit, nach der fehlgeschlagene Anmeldeversuche zurückgesetzt werden"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF-Schutz deaktivieren"
description = "Cross-Site Request Forgery-Schutz deaktivieren (nicht empfohlen)"
@@ -950,6 +950,7 @@ title = "Αυτόματο σπάσιμο με μέγεθος/σελ."
desc = "Διαχωρισμός ενός PDF σε πολλαπλά έγγραφα βάσει μεγέθους, αριθμού σελίδων ή αριθμού εγγράφων"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Αλλαγή & αντιστροφή χρώματος"
desc = "Αντικαταστήστε ή αντιστρέψτε χρώματα σε έγγραφα PDF"
@@ -964,18 +965,22 @@ title = "Αυτόματη σάρωση φακέλων"
desc = "Σύνδεσμος προς τον οδηγό αυτοματοποιημένης σάρωσης φακέλων"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Οδηγός SSO"
desc = "Σύνδεσμος προς τον οδηγό SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Ρύθμιση Air-gapped"
desc = "Σύνδεσμος προς τον οδηγό ρύθμισης Air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Προσθήκη κωδικού"
desc = "Κρυπτογράφηση του εγγράφου PDF με κωδικό."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Αλλαγή δικαιωμάτων"
desc = "Αλλαγή περιορισμών και δικαιωμάτων εγγράφου"
@@ -985,10 +990,12 @@ title = "Αυτοματοποίηση"
desc = "Δημιουργήστε ροές πολλών βημάτων συνδέοντας ενέργειες PDF. Ιδανικό για επαναλαμβανόμενες εργασίες."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Επικάλυψη PDF πάνω σε άλλο PDF"
title = "Επικάλυψη PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Επεξεργαστής κειμένου PDF"
desc = "Επιθεωρήστε και επεξεργαστείτε εξαγωγές JSON του Stirling PDF με ομαδοποιημένη επεξεργασία κειμένου και αναδημιουργία PDF"
@@ -4526,6 +4533,13 @@ description = "Μέγιστος αριθμός αποτυχημένων προσ
label = "Χρόνος επαναφοράς σύνδεσης (λεπτά)"
description = "Χρόνος πριν μηδενιστούν οι αποτυχημένες προσπάθειες σύνδεσης"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Απενεργοποίηση προστασίας CSRF"
description = "Απενεργοποίηση προστασίας Cross-Site Request Forgery (δεν συνιστάται)"
+69 -55
View File
@@ -690,257 +690,257 @@ workbenchSlide = "Workspace panel"
workspace = "Workspace"
[home.multiTool]
tags = "multiple,tools"
tags = "multiple,tools,multi-tool,all-in-one,swiss army,page organizer,page editor,edit pages,manage pages,organize,reorganize"
title = "PDF Multi Tool"
desc = "Merge, Rotate, Rearrange, Split, and Remove pages"
[home.merge]
tags = "combine,join,unite"
tags = "combine,join,unite,merge,merge PDFs,combine PDFs,join PDFs,concatenate,append,stitch,combine files,join files,merge documents"
title = "Merge"
desc = "Easily merge multiple PDFs into one."
[home.split]
tags = "divide,separate,break"
tags = "divide,separate,break,split,extract pages,separate pages,divide document,break apart,separate files,unbind,split by page,divide by chapter"
title = "Split"
desc = "Split PDFs into multiple documents"
[home.rotate]
tags = "turn,flip,orient"
tags = "turn,flip,orient,rotate,orientation,landscape,portrait,90 degrees,180 degrees,clockwise,anticlockwise,counter-clockwise,fix orientation"
title = "Rotate"
desc = "Easily rotate your PDFs."
[home.convert]
tags = "transform,change"
tags = "transform,change,convert,PDF to Word,PDF to Excel,PDF to image,Word to PDF,Excel to PDF,PowerPoint to PDF,HTML to PDF,export,import,file conversion,format change,save as"
title = "Convert"
desc = "Convert files between different formats"
[home.pdfOrganiser]
tags = "organize,rearrange,reorder"
tags = "organize,rearrange,reorder,organise,arrange pages,sort,move pages,delete pages,remove pages,page management,page organizer,page organiser,resequence"
title = "Organise"
desc = "Remove/Rearrange pages in any order"
[home.addImage]
tags = "insert,embed,place"
tags = "insert,embed,place,add image,insert image,place image,embed image,add photo,add picture,add logo,graphics,insert picture,place photo,PNG,JPG,JPEG"
title = "Add image"
desc = "Adds a image onto a set location on the PDF"
[home.addAttachments]
tags = "embed,attach,include"
tags = "embed,attach,include,attachments,attach files,embed files,include files,add files,file attachment,associated files,supplementary files"
title = "Add Attachments"
desc = "Add or remove embedded files (attachments) to/from a PDF"
[home.watermark]
tags = "stamp,mark,overlay"
tags = "stamp,mark,overlay,watermark,branding,logo,confidential,draft,copyright,trademark,text overlay,image overlay,background text"
title = "Add Watermark"
desc = "Add a custom watermark to your PDF document."
[home.removePassword]
tags = "unlock"
tags = "unlock,remove password,unlock PDF,decrypt,remove encryption,unprotect,open protected PDF,password removal,unlock protected,disable password,remove security,remove owner password"
title = "Remove Password"
desc = "Remove password protection from your PDF document."
[home.compress]
tags = "shrink,reduce,optimize"
tags = "shrink,reduce,optimize,compress,smaller,downsize,file size,reduce size,minimize,make smaller,decrease size,optimize size"
title = "Compress"
desc = "Compress PDFs to reduce their file size."
[home.unlockPDFForms]
tags = "unlock,enable,edit"
tags = "unlock,enable,edit,unlock forms,enable forms,editable forms,remove read only,make editable,unlock fields,enable editing,form fields,fillable,unprotect forms"
title = "Unlock PDF Forms"
desc = "Remove read-only property of form fields in a PDF document."
[home.changeMetadata]
tags = "edit,modify,update"
tags = "edit,modify,update,metadata,properties,document properties,author,title,subject,keywords,creator,producer,info,document info,file properties"
title = "Change Metadata"
desc = "Change/Remove/Add metadata from a PDF document"
[home.ocr]
tags = "extract,scan"
tags = "extract,scan,OCR,optical character recognition,text recognition,scan to text,image to text,scanned document,searchable PDF,make searchable,extract text,recognize text,read scanned"
title = "OCR / Cleanup scans"
desc = "Cleanup scans and detects text from images within a PDF and re-adds it as text."
[home.extractImages]
tags = "pull,save,export"
tags = "pull,save,export,extract images,get images,save images,export images,extract photos,extract pictures,pull images,download images,rip images,extract graphics,save photos"
title = "Extract Images"
desc = "Extracts all images from a PDF and saves them to zip"
[home.scannerImageSplit]
tags = "detect,split,photos"
tags = "detect,split,photos,auto detect,detect photos,split photos,separate photos,split scanned images,multiple photos,auto split,photo detection,image detection,scan separation"
title = "Detect & Split Scanned Photos"
desc = "Detect and split scanned photos into separate pages"
[home.sign]
tags = "signature,autograph"
tags = "signature,autograph,e-sign,electronic signature,digital signature,sign document,approval,signoff,authorize,endorse,ink signature,handwriting"
title = "Sign"
desc = "Adds signature to PDF by drawing, text or image"
[home.annotate]
tags = "annotate,highlight,draw"
tags = "annotate,highlight,draw,markup,comment,notes,review,redline,feedback,markup tools,sticky notes,shapes,arrows,text box,freehand"
title = "Annotate"
desc = "Highlight, draw, add notes and shapes in the viewer"
[home.flatten]
tags = "simplify,remove,interactive"
tags = "simplify,remove,interactive,flatten,flatten form,remove form fields,make static,finalize form,lock form,disable editing,convert to image,non-editable"
title = "Flatten"
desc = "Remove all interactive elements and forms from a PDF"
[home.certSign]
tags = "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto"
tags = "authenticate,PEM,P12,official,encrypt,sign,certificate,PKCS12,JKS,server,manual,auto,digital certificate,certificate signature,PKI,cryptographic signature,trusted signature"
title = "Sign with Certificate"
desc = "Signs a PDF with a Certificate/Key (PEM/P12)"
[home.repair]
tags = "fix,restore"
tags = "fix,restore,repair,fix PDF,fix broken,fix corrupt,repair PDF,repair corrupt,broken PDF,corrupt PDF,damaged PDF,recover,fix errors,PDF won't open,can't open PDF,PDF errors,troubleshoot,restore PDF,rebuild,corrupted"
title = "Repair"
desc = "Tries to repair a corrupt/broken PDF"
[home.removeBlanks]
tags = "delete,clean,empty"
tags = "delete,clean,empty,remove blank,delete blank pages,empty pages,white pages,remove empty,clean up,cleanup blank"
title = "Remove Blank pages"
desc = "Detects and removes blank pages from a document"
[home.removeAnnotations]
tags = "delete,clean,strip"
tags = "delete,clean,strip,remove annotations,remove comments,delete comments,remove markup,remove highlights,clean annotations,strip comments,remove notes,delete markup,clear comments"
title = "Remove Annotations"
desc = "Removes all comments/annotations from a PDF"
[home.compare]
tags = "difference"
tags = "difference,compare,diff,compare PDFs,compare documents,find differences,show differences,changes,what changed,track changes,revisions,version compare,side by side,contrast,delta"
title = "Compare"
desc = "Compares and shows the differences between 2 PDF Documents"
[home.removeCertSign]
tags = "remove,delete,unlock"
tags = "remove,delete,unlock,remove certificate,remove signature,delete signature,unsigned,remove digital signature,strip signature,remove cert,unsign"
title = "Remove Certificate Sign"
desc = "Remove certificate signature from PDF"
[home.pageLayout]
tags = "layout,arrange,combine"
tags = "layout,arrange,combine,N-up,2-up,4-up,multiple per page,pages per sheet,layout pages,tile,grid layout,multi-page layout,combine on page,handout"
title = "Multi-Page Layout"
desc = "Merge multiple pages of a PDF document into a single page"
[home.bookletImposition]
tags = "booklet,print,binding"
tags = "booklet,print,binding,imposition,booklet printing,saddle stitch,fold,pamphlet,brochure,print booklet,duplex,two-sided,signature,book layout,page imposition,print layout"
title = "Booklet Imposition"
desc = "Create booklets with proper page ordering and multi-page layout for printing and binding"
[home.scalePages]
tags = "resize,adjust,scale"
tags = "resize,adjust,scale,page size,resize page,scale page,change size,adjust size,enlarge,shrink page,fit to page,A4,letter size"
title = "Adjust page size/scale"
desc = "Change the size/scale of a page and/or its contents."
[home.addPageNumbers]
tags = "number,pagination,count"
tags = "number,pagination,count,add page numbers,page numbering,page numbers,footer,header,number pages,sequential,pagination tool"
title = "Add Page Numbers"
desc = "Add Page numbers throughout a document in a set location"
[home.autoRename]
tags = "auto-detect,header-based,organize,relabel"
tags = "auto-detect,header-based,organize,relabel,auto rename,automatic rename,smart rename,rename by content,filename,file naming,detect title"
title = "Auto Rename PDF File"
desc = "Auto renames a PDF file based on its detected header"
[home.adjustContrast]
tags = "contrast,brightness,saturation"
tags = "contrast,brightness,saturation,adjust colors,color correction,enhance,lighten,darken,improve quality,color balance,hue,vibrance"
title = "Adjust Colours/Contrast"
desc = "Adjust Colors/Contrast, Saturation and Brightness of a PDF"
[home.crop]
tags = "trim,cut,resize"
tags = "trim,cut,resize,crop,crop PDF,trim PDF,trim margins,remove margins,cut edges,trim borders,remove white space,crop pages,trim pages,reduce margins,set margins"
title = "Crop PDF"
desc = "Crop a PDF to reduce its size (maintains text!)"
[home.autoSplitPDF]
tags = "auto,split,QR"
tags = "auto,split,QR,auto split,QR code,QR split,barcode,automatic split,divider page,separator page,scan divider,batch scanning"
title = "Auto Split Pages"
desc = "Auto Split Scanned PDF with physical scanned page splitter QR Code"
[home.sanitize]
tags = "clean,purge,remove"
tags = "clean,purge,remove,sanitize,sanitise,remove scripts,remove javascript,remove metadata,strip metadata,security,clean document,remove hidden data,privacy"
title = "Sanitise"
desc = "Remove potentially harmful elements from PDF files"
[home.getPdfInfo]
tags = "info,metadata,details"
tags = "info,metadata,details,PDF info,document info,properties,file info,get info,show info,view properties,document properties,statistics,page count,file details,inspect"
title = "Get ALL Info on PDF"
desc = "Grabs any and all information possible on PDFs"
[home.pdfToSinglePage]
tags = "combine,merge,single"
tags = "combine,merge,single,single page,one page,merge to single,combine all,stitch pages,concatenate vertical,long page,poster"
title = "PDF to Single Large Page"
desc = "Merges all PDF pages into one large single page"
[home.showJS]
tags = "javascript,code,script"
tags = "javascript,code,script,show javascript,show JS,find javascript,detect javascript,view javascript,embedded scripts,malware,security,inspect,debug"
title = "Show Javascript"
desc = "Searches and displays any JS injected into a PDF"
[home.redact]
tags = "censor,blackout,hide"
tags = "censor,blackout,hide,redact,redaction,black out,block out,remove sensitive,hide text,privacy,confidential,GDPR,PII,sensitive data,permanently remove,cover up,legal redaction"
title = "Redact"
desc = "Redacts (blacks out) a PDF based on selected text, drawn shapes and/or selected page(s)"
[home.splitBySections]
tags = "split,sections,divide"
tags = "split,sections,divide,split by sections,grid split,divide pages,split into sections,cut pages,divide grid,section split,horizontal split,vertical split"
title = "Split PDF by Sections"
desc = "Divide each page of a PDF into smaller horizontal and vertical sections"
[home.addStamp]
tags = "stamp,mark,seal"
tags = "stamp,mark,seal,approved,rejected,confidential,stamp tool,rubber stamp,date stamp,approval stamp,received,void,copy,original"
title = "Add Stamp to PDF"
desc = "Add text or add image stamps at set locations"
[home.removeImage]
tags = "remove,delete,clean"
tags = "remove,delete,clean,remove image,delete image,strip images,remove pictures,delete photos,clean images,reduce size,remove graphics"
title = "Remove image"
desc = "Remove image from PDF to reduce file size"
[home.splitByChapters]
tags = "split,chapters,structure"
tags = "split,chapters,structure,split by chapters,split by bookmarks,bookmarks,outline,table of contents,TOC split,chapter split,divide by sections"
title = "Split PDF by Chapters"
desc = "Split a PDF into multiple files based on its chapter structure."
[home.validateSignature]
tags = "validate,verify,certificate"
tags = "validate,verify,certificate,validate signature,verify signature,check signature,digital signature,certificate verification,signature validation,authentic,trust,signed,verify certificate"
title = "Validate PDF Signature"
desc = "Verify digital signatures and certificates in PDF documents"
[home.swagger]
tags = "API,documentation,test"
tags = "API,documentation,test,swagger,API docs,REST API,endpoints,developer,API reference,API testing,OpenAPI,integration,developer docs"
title = "API Documentation"
desc = "View API documentation and test endpoints"
[home.scannerEffect]
tags = "scan,simulate,create"
tags = "scan,simulate,create,fake scan,look scanned,scanner effect,make look scanned,photocopy effect,simulate scanner,realistic scan"
title = "Scanner Effect"
desc = "Create a PDF that looks like it was scanned"
[home.editTableOfContents]
tags = "bookmarks,contents,edit"
tags = "bookmarks,contents,edit,table of contents,TOC,outline,navigation,chapters,sections,add bookmarks,edit bookmarks,PDF outline"
title = "Edit Table of Contents"
desc = "Add or edit bookmarks and table of contents in PDF documents"
[home.manageCertificates]
tags = "certificates,import,export"
tags = "certificates,import,export,manage certificates,digital certificates,certificate management,PFX,P12,keystore,import certificate,export certificate,certificate store,PKI"
title = "Manage Certificates"
desc = "Import, export, or delete digital certificate files used for signing PDFs."
[home.read]
tags = "view,open,display"
tags = "view,open,display,read,viewer,PDF viewer,PDF reader,open PDF,view PDF,display PDF,preview,browse"
title = "Read"
desc = "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
[home.reorganizePages]
tags = "rearrange,reorder,organize"
tags = "rearrange,reorder,organize,reorganize,move pages,page order,sort pages,arrange pages,shuffle,resequence"
title = "Reorganize Pages"
desc = "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
[home.extractPages]
tags = "pull,select,copy"
tags = "pull,select,copy,extract,extract pages,get pages,pull out,save pages,export pages,copy pages,select pages,specific pages"
title = "Extract Pages"
desc = "Extract specific pages from a PDF document"
[home.removePages]
tags = "delete,extract,exclude"
tags = "delete,extract,exclude,remove pages,delete pages,remove page,delete page,exclude pages,take out pages,discard pages,drop pages"
title = "Remove Pages"
desc = "Remove specific pages from a PDF document"
@@ -950,50 +950,57 @@ title = "Auto Split by Size/Count"
desc = "Automatically split PDFs by file size or page count"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Replace & Invert Colour"
desc = "Replace or invert colours in PDF documents"
[home.devApi]
tags = "API,development,documentation"
tags = "API,development,documentation,developer,REST,integration,endpoints,programmatic,automation,scripting"
title = "API"
desc = "Link to API documentation"
[home.devFolderScanning]
tags = "automation,folder,scanning"
tags = "automation,folder,scanning,watch folder,hot folder,automatic processing,batch,monitor folder,auto process,folder monitoring"
title = "Automated Folder Scanning"
desc = "Link to automated folder scanning guide"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO Guide"
desc = "Link to SSO guide"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped Setup"
desc = "Link to air-gapped setup guide"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Add Password"
desc = "Add password protection and restrictions to PDF files"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Change Permissions"
desc = "Change document restrictions and permissions"
[home.automate]
tags = "workflow,sequence,automation"
tags = "workflow,sequence,automation,automate,batch,batch processing,pipeline,chain,multi-step,recurring,scheduled,automatic,process multiple,bulk operations"
title = "Automate"
desc = "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Overlay one PDF on top of another"
title = "Overlay PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF Text Editor"
desc = "Edit existing text and images inside PDFs"
[home.addText]
tags = "text,annotation,label"
tags = "text,annotation,label,add text,insert text,place text,text box,add label,add caption,type on PDF,write on PDF,add words,add note,text overlay,typewriter"
title = "Add Text"
desc = "Add custom text anywhere in your PDF"
@@ -4527,6 +4534,13 @@ description = "Maximum number of failed login attempts before account lockout"
label = "Login Reset Time (minutes)"
description = "Time before failed login attempts are reset"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Disable CSRF Protection"
description = "Disable Cross-Site Request Forgery protection (not recommended)"
@@ -950,6 +950,7 @@ title = "Auto dividir por tamaño/conteo"
desc = "Divide un solo PDF en múltiples documentos según su tamaño, número de páginas, o número de documento"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Reemplazar e Invertir Color"
desc = "Reemplace o invierta colores en documentos PDF"
@@ -964,18 +965,22 @@ title = "Escaneo Automatizado de Carpetas"
desc = "Enlace a la guía de escaneo automatizado de carpetas"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guía de SSO"
desc = "Enlace a la guía de SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Configuración Aislada"
desc = "Enlace a la guía de configuración aislada"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Proteger con contraseña"
desc = "Cifrar documento PDF con contraseña"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Cambiar permisos"
desc = "Modificar restricciones y permisos del documento"
@@ -985,10 +990,12 @@ title = "Automatizar"
desc = "Crear flujos de trabajo de múltiples pasos encadenando acciones de PDF. Ideal para tareas recurrentes."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Superponer PDFs encima de otro PDF"
title = "Superponer PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor de texto PDF"
desc = "Revise y edite exportaciones JSON de Stirling PDF con edición de texto agrupada y regeneración de PDF"
@@ -4526,6 +4533,13 @@ description = "Número máximo de intentos fallidos antes de bloquear la cuenta"
label = "Tiempo de restablecimiento del inicio de sesión (minutos)"
description = "Tiempo antes de que se restablezcan los intentos fallidos de inicio de sesión"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Desactivar la protección CSRF"
description = "Desactivar la protección contra Cross-Site Request Forgery (no recomendado)"
@@ -950,6 +950,7 @@ title = "Autom. zatitu tamaina/kop."
desc = "Split a single PDF into multiple documents based on size, page count, or document count"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Ordeztu eta inbertitu kolorea"
desc = "Ordeztu edo alderantzikatu koloreak PDF dokumentuetan"
@@ -964,18 +965,22 @@ title = "Karpeta eskaneatze autom."
desc = "Esteka karpeta eskaneatze automatizatuaren gidara"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO gida"
desc = "Esteka SSO gidara"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Sare isolatuko konfigurazioa"
desc = "Esteka sare isolatutako konfigurazio gidara"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Gehitu pasahitza"
desc = "Enkriptatu PDF dokumentua pasahitz batekin"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Aldatu baimenak"
desc = "Aldatu dokumentuaren murrizketak eta baimenak"
@@ -985,10 +990,12 @@ title = "Automatizatu"
desc = "Eraiki hainbat pausotako workflowak PDF ekintzak kateatuz. Egokia zeregin errepikakorretarako."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Overlays PDFs on-top of another PDF"
title = "Gainjarri PDFak"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF testu-editorea"
desc = "Berrikusi eta editatu Stirling PDF JSON esportazioak taldekatutako testu-edizioarekin eta PDF birsorkuntzarekin"
@@ -4526,6 +4533,13 @@ description = "Kontua blokeatu aurretik huts egindako saio-saiakeren gehienezko
label = "Saio-hasieraren berrezartze denbora (minutuak)"
description = "Huts egindako saio-saiakerak berrezarri aurreko denbora"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF babesa desgaitu"
description = "Desgaitu Cross-Site Request Forgery babesa (ez da gomendatzen)"
@@ -950,6 +950,7 @@ title = "تقسیم خودکار بر اساس اندازه/تعداد"
desc = "تقسیم یک PDF به چند سند بر اساس اندازه، تعداد صفحات، یا تعداد اسناد"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "جایگزینی و معکوس کردن رنگ"
desc = "جایگزینی یا معکوس کردن رنگ‌ها در اسناد PDF"
@@ -964,18 +965,22 @@ title = "اسکن خودکار پوشه"
desc = "پیوند به راهنمای اسکن خودکار پوشه"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "راهنمای SSO"
desc = "پیوند به راهنمای SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "راه‌اندازی Air-gapped"
desc = "پیوند به راهنمای راه‌اندازی Air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "افزودن رمز عبور"
desc = "رمزگذاری سند PDF شما با رمز عبور."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "تغییر مجوزها"
desc = "تغییر محدودیت‌ها و مجوزهای سند"
@@ -985,10 +990,12 @@ title = "اتوماسیون"
desc = "ساخت گردش‌کارهای چندمرحله‌ای با زنجیره کردن اقدامات PDF. مناسب برای کارهای تکرارشونده."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF‌ها را بر روی PDF دیگری هم‌پوشانی می‌کند"
title = "هم‌پوشانی PDF‌ها"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "ویرایشگر متن PDF"
desc = "بازبینی و ویرایش خروجی‌های JSON Stirling PDF با ویرایش گروهی متن و بازتولید PDF"
@@ -4526,6 +4533,13 @@ description = "حداکثر تعداد تلاش ناموفق ورود قبل ا
label = "زمان بازنشانی ورود (دقیقه)"
description = "مدتی که پس از آن تلاش‌های ناموفق ورود بازنشانی می‌شوند"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "غیرفعال کردن محافظت CSRF"
description = "غیرفعال کردن محافظت Cross-Site Request Forgery (توصیه نمی‌شود)"
@@ -950,6 +950,7 @@ title = "Scinder auto taille/pages"
desc = "Séparer un PDF unique en plusieurs documents en fonction de la taille, du nombre de pages ou du nombre de documents."
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Remplacer & inverser couleurs"
desc = "Remplacer ou inverser les couleurs dans les documents PDF"
@@ -964,18 +965,22 @@ title = "Scan auto de dossiers"
desc = "Lien vers le guide d’analyse de dossier automatisée"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guide SSO"
desc = "Lien vers le guide SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Installation isolée"
desc = "Lien vers le guide d’installation isolée (air-gapped)"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Ajouter un mot de passe"
desc = "Chiffrez votre PDF avec un mot de passe."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Modifier les permissions"
desc = "Modifier les restrictions et permissions du document"
@@ -985,10 +990,12 @@ title = "Automatiser"
desc = "Créez des workflows multi-étapes en enchaînant des actions PDF. Idéal pour les tâches récurrentes."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Superposer un PDF sur un autre"
title = "Superposer des PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Éditeur de texte PDF"
desc = "Afficher et modifier les exports JSON de Stirling PDF avec édition de texte groupée et régénération du PDF"
@@ -4526,6 +4533,13 @@ description = "Nombre maximal de tentatives de connexion échouées avant le ver
label = "Délai de réinitialisation (minutes)"
description = "Délai avant la réinitialisation du compteur de tentatives de connexion échouées"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Désactiver la protection CSRF"
description = "Désactiver la protection contre la falsification de requête intersites (non recommandé)"
@@ -950,6 +950,7 @@ title = "Scoilt auto: méid/líon"
desc = "Scoilt PDF amháin i ndoiciméid iolracha bunaithe ar mhéid, líon na leathanach, nó comhaireamh doiciméad"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Ionadaigh & Inbhéartaigh Dath"
desc = "Ionadaigh nó inbhéartaigh dathanna i gcáipéisí PDF"
@@ -964,18 +965,22 @@ title = "Scanadh Fillteán Uathoibríoch"
desc = "Nasc le treoir scantha fillteán uathoibrithe"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Treoir SSO"
desc = "Nasc le treoir SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Socrú Air-gapped"
desc = "Nasc le treoir socraithe Air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Cuir Pasfhocal leis"
desc = "Criptigh do dhoiciméad PDF le focal faire."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Athrú Ceadanna"
desc = "Athraigh srianta agus ceadanna cáipéise"
@@ -985,10 +990,12 @@ title = "Uathoibrigh"
desc = "Tóg sreafaí oibre ilchéime trí ghníomhartha PDF a nascadh le chéile. Foirfe do thascanna athfhillteacha."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Forleagain PDF ar bharr PDF eile"
title = "Forleagan PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Eagarthóir Téacs PDF"
desc = "Cuir téacs agus íomhánna atá ann cheana in eagar laistigh de PDFanna"
@@ -4526,6 +4533,13 @@ description = "Uasmhéid iarrachtaí logála isteach teipthe sula gcuirtear an c
label = "Am Athshocraithe Logála Isteach (nóiméid)"
description = "Am sula n‑athshocraítear iarrachtaí logála isteach teipthe"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Díchumasaigh Cosaint CSRF"
description = "Díchumasaigh cosaint Cross-Site Request Forgery (ní mholtar)"
@@ -950,6 +950,7 @@ title = "आकार/गिनती से ऑटो बाँटें"
desc = "एक PDF को आकार, पृष्ठ संख्या, या दस्तावेज़ संख्या के आधार पर कई दस्तावेज़ों में विभाजित करें"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "रंग बदलें/उलटें"
desc = "PDF दस्तावेज़ों में रंगों को प्रतिस्थापित या उलटें"
@@ -964,18 +965,22 @@ title = "स्वचालित फ़ोल्डर स्कैनिंग
desc = "स्वचालित फ़ोल्डर स्कैनिंग गाइड के लिए लिंक"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO गाइड"
desc = "SSO गाइड के लिए लिंक"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "एयर-गैप्ड सेटअप"
desc = "एयर-गैप्ड सेटअप गाइड के लिए लिंक"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "पासवर्ड जोड़ें"
desc = "पासवर्ड के साथ अपने PDF दस्तावेज को एन्क्रिप्ट करें।"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "अनुमतियां बदलें"
desc = "दस्तावेज़ प्रतिबंध और अनुमतियाँ बदलें"
@@ -985,10 +990,12 @@ title = "स्वचालित करें"
desc = "PDF क्रियाओं को जोड़कर बहु-चरणीय वर्कफ़्लो बनाएँ। बार-बार होने वाले कार्यों के लिए उपयुक्त।"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF को दूसरी PDF के ऊपर ओवरले करें"
title = "PDF ओवरले करें"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF टेक्स्ट एडिटर"
desc = "ग्रुप्ड टेक्स्ट एडिटिंग और PDF पुनर्जनन के साथ Stirling PDF JSON एक्सपोर्ट की समीक्षा व संपादन करें"
@@ -4526,6 +4533,13 @@ description = "खाते को लॉक करने से पहले
label = "लॉगिन रीसेट समय (मिनट)"
description = "वह समय जिसके बाद असफल लॉगिन प्रयास रीसेट हो जाते हैं"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF सुरक्षा निष्क्रिय करें"
description = "Cross-Site Request Forgery सुरक्षा निष्क्रिय करें (अनुशंसित नहीं)"
@@ -950,6 +950,7 @@ title = "Auto dijeli po veličini/broju"
desc = "Podijelite jedan PDF na više dokumenata na temelju veličine, broja stranica ili broja dokumenata"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Zamijeni i invertiraj boju"
desc = "Zamijenite ili invertirajte boje u PDF dokumentima"
@@ -964,18 +965,22 @@ title = "Auto skeniranje mapa"
desc = "Poveznica na vodič za automatizirano skeniranje mapa"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO vodič"
desc = "Poveznica na SSO vodič"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped postavljanje"
desc = "Poveznica na vodič za air-gapped postavljanje"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Dodaj lozinku"
desc = "Šifrirajte svoj PDF dokument lozinkom.."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Promjena dopuštenja"
desc = "Promijenite ograničenja dokumenta i dopuštenja"
@@ -985,10 +990,12 @@ title = "Automatiziraj"
desc = "Izgradite višekoračne tijekove rada povezivanjem PDF radnji. Idealno za ponavljajuće zadatke."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Preklapa PDF-ove na drugi PDF"
title = "Preklapanje PDF-ova"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF uređivač teksta"
desc = "Pregledajte i uredite Stirling PDF JSON izvoze s grupnim uređivanjem teksta i ponovnim generiranjem PDF-a"
@@ -4526,6 +4533,13 @@ description = "Maksimalan broj neuspjelih pokušaja prijave prije zaključavanja
label = "Vrijeme resetiranja prijave (minute)"
description = "Vrijeme prije resetiranja neuspjelih pokušaja prijave"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Onemogući zaštitu od CSRF-a"
description = "Onemogući zaštitu od Cross-Site Request Forgery (ne preporučuje se)"
@@ -950,6 +950,7 @@ title = "Auto felosztás méret/darab"
desc = "Egyetlen PDF felosztása több dokumentumra méret, oldalszám vagy dokumentumszám alapján"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Színek cseréje és invertálása"
desc = "Színek cseréje vagy invertálása PDF dokumentumokban"
@@ -964,18 +965,22 @@ title = "Automatikus mappaszkennelés"
desc = "Hivatkozás az automatikus mappaszkennelés útmutatójára"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO útmutató"
desc = "Hivatkozás az SSO útmutatóra"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped beállítás"
desc = "Hivatkozás az elszigetelt környezet beállítási útmutatójára"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Jelszó hozzáadása"
desc = "PDF dokumentum jelszavas védelme"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Jogosultságok módosítása"
desc = "Dokumentumkorlátozások és jogosultságok módosítása"
@@ -985,10 +990,12 @@ title = "Automatizálás"
desc = "Többlépéses munkafolyamatok összeállítása PDF műveletek összefűzésével. Ideális ismétlődő feladatokhoz."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF-ek egymásra helyezése egy másik PDF-en"
title = "PDF-ek egymásra helyezése"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF szövegszerkesztő"
desc = "Nézze át és szerkessze a Stirling PDF JSON exportokat csoportosított szövegszerkesztéssel és PDF-újragenerálással"
@@ -4526,6 +4533,13 @@ description = "Sikertelen bejelentkezési kísérletek maximális száma fiókz
label = "Visszaállítás ideje (perc)"
description = "Idő, ami után a sikertelen bejelentkezési kísérletek számlálója lenullázódik"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF védelem letiltása"
description = "Cross-Site Request Forgery védelem letiltása (nem ajánlott)"
@@ -950,6 +950,7 @@ title = "Pisah otomatis ukuran/jumlah"
desc = "Membagi satu PDF menjadi beberapa dokumen berdasarkan ukuran, jumlah halaman, atau jumlah dokumen"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Ganti & Balik Warna"
desc = "Ganti atau balik warna dalam dokumen PDF"
@@ -964,18 +965,22 @@ title = "Pemindaian Folder Otomatis"
desc = "Tautan ke panduan pemindaian folder otomatis"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Panduan SSO"
desc = "Tautan ke panduan SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Penyiapan Air-gapped"
desc = "Tautan ke panduan penyiapan air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Tambahkan Kata Sandi"
desc = "Enkripsi dokumen PDF Anda dengan kata sandi."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Ganti Perizinan"
desc = "Ubah pembatasan dan izin dokumen"
@@ -985,10 +990,12 @@ title = "Otomasi"
desc = "Bangun alur kerja multi-langkah dengan merangkai tindakan PDF. Ideal untuk tugas berulang."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Menumpuk PDF di atas PDF lain"
title = "Tumpuk PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor Teks PDF"
desc = "Tinjau dan edit ekspor Stirling PDF JSON dengan pengeditan teks terkelompok dan pembuatan ulang PDF"
@@ -4526,6 +4533,13 @@ description = "Jumlah maksimum kegagalan login sebelum akun terkunci"
label = "Waktu Reset Login (menit)"
description = "Waktu sebelum upaya login yang gagal direset"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Nonaktifkan Perlindungan CSRF"
description = "Nonaktifkan perlindungan Cross-Site Request Forgery (tidak disarankan)"
@@ -950,6 +950,7 @@ title = "Dividi auto per peso/pagine"
desc = "Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Sostituisci e inverti colore"
desc = "Sostituisci o inverti i colori nei documenti PDF"
@@ -964,18 +965,22 @@ title = "Scansione automatica cartelle"
desc = "Link alla guida per scansione cartelle automatizzata"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guida SSO"
desc = "Link alla guida SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Setup isolato (air‑gapped)"
desc = "Link alla guida per setup air‑gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Aggiungi Password"
desc = "Crittografa il tuo PDF con una password."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Cambia Permessi"
desc = "Modifica restrizioni e permessi del documento"
@@ -985,10 +990,12 @@ title = "Automatizza"
desc = "Crea flussi multi‑step concatenando azioni PDF. Ideale per attività ricorrenti."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Sovrapponi un PDF sopra un altro"
title = "Sovrapponi PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor di testo PDF"
desc = "Modifica testo e immagini esistenti nei PDF"
@@ -4526,6 +4533,13 @@ description = "Numero massimo di tentativi di accesso falliti prima del blocco d
label = "Tempo di reset accessi (minuti)"
description = "Tempo prima che i tentativi di accesso falliti vengano azzerati"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Disabilita protezione CSRF"
description = "Disabilita la protezione Cross-Site Request Forgery (non raccomandato)"
@@ -950,6 +950,7 @@ title = "サイズ・数による自動分割"
desc = "サイズ・ページ数またはドキュメント数に基づいて、1つのPDFを複数のドキュメントに分割します。"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "色の置換と反転"
desc = "PDF 文書の色を置換または反転"
@@ -964,18 +965,22 @@ title = "自動フォルダスキャン"
desc = "自動フォルダスキャン ガイドへのリンク"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO ガイド"
desc = "SSO ガイドへのリンク"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "エアギャップ設定"
desc = "エアギャップ環境のセットアップガイドへのリンク"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "パスワードの追加"
desc = "PDFをパスワードで暗号化します。"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "権限の変更"
desc = "文書の制限と権限を変更"
@@ -985,10 +990,12 @@ title = "自動化"
desc = "PDF アクションを連結して複数ステップのワークフローを構築。繰り返し作業に最適です。"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "1つのPDFを別のPDFの上に重ねます"
title = "PDFを重ね合わせ"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDFテキストエディター"
desc = "グループ化されたテキスト編集とPDF再生成で、Stirling PDF の JSON エクスポートをレビュー・編集します。"
@@ -4526,6 +4533,13 @@ description = "アカウントロックまでの最大失敗回数"
label = "ログインリセット時間(分)"
description = "失敗回数がリセットされるまでの時間"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF 保護を無効化"
description = "クロスサイトリクエストフォージェリ保護を無効化(非推奨)"
@@ -950,6 +950,7 @@ title = "크기/개수별 자동 분할"
desc = "단일 PDF를 크기, 페이지 수 또는 문서 수를 기준으로 여러 문서로 분할"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "색상 교체 및 반전"
desc = "PDF 문서의 색상을 교체하거나 반전합니다."
@@ -964,18 +965,22 @@ title = "자동 폴더 스캔"
desc = "자동 폴더 스캔 가이드로 이동"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO 가이드"
desc = "SSO 가이드로 이동"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "격리 환경 설정"
desc = "격리 환경 설정 가이드로 이동"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "비밀번호 추가"
desc = "PDF 문서를 비밀번호로 암호화합니다."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "권한 변경"
desc = "문서 제한 및 권한 변경"
@@ -985,10 +990,12 @@ title = "자동화"
desc = "PDF 작업을 연결하여 다단계 워크플로를 구성하세요. 반복 작업에 이상적입니다."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF를 다른 PDF 위에 오버레이"
title = "PDF 오버레이"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF 텍스트 편집기"
desc = "그룹화된 텍스트 편집과 PDF 재생성으로 Stirling PDF의 JSON 내보내기를 검토하고 편집하세요"
@@ -4526,6 +4533,13 @@ description = "계정 잠금 전 허용되는 최대 로그인 실패 횟수"
label = "로그인 재설정 시간(분)"
description = "로그인 실패 횟수가 재설정되기까지의 시간"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF 보호 비활성화"
description = "교차 사이트 요청 위조 보호를 비활성화합니다(권장하지 않음)"
@@ -950,6 +950,7 @@ title = "സൈസ്/കൗണ്ട് ഓട്ടോ സ്പ്ലിറ
desc = "ഫയൽ വലുപ്പം അല്ലെങ്കിൽ പേജ് എണ്ണം പ്രകാരം PDF-കൾ സ്വയം വിഭജിക്കുക"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "നിറം മാറ്റുക / ഇൻവേർട്ട്"
desc = "PDF ഡോക്യുമെന്റുകളിൽ നിറങ്ങൾ പകരംവെക്കുകയോ ഇൻവേർട്ട് ചെയ്യുകയോ ചെയ്യുക"
@@ -964,18 +965,22 @@ title = "ഓട്ടോമേറ്റഡ് ഫോൾഡർ സ്കാനി
desc = "ഓട്ടോമേറ്റഡ് ഫോൾഡർ സ്കാനിംഗ് ഗൈഡിലേക്കുള്ള ലിങ്ക്"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO ഗൈഡ്"
desc = "SSO ഗൈഡിലേക്കുള്ള ലിങ്ക്"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped സെറ്റപ്പ്"
desc = "എയർ-ഗ്യാപ്ഡ് സെറ്റപ്പ് ഗൈഡിലേക്കുള്ള ലിങ്ക്"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "പാസ്‌വേഡ് ചേർക്കുക"
desc = "നിങ്ങളുടെ PDF പ്രമാണം ഒരു പാസ്‌വേഡ് ഉപയോഗിച്ച് എൻക്രിപ്റ്റ് ചെയ്യുക."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "അനുമതികൾ മാറ്റുക"
desc = "ഡോക്യുമെന്റ് നിയന്ത്രണങ്ങളും അനുമതികളും മാറ്റുക"
@@ -985,10 +990,12 @@ title = "ഓട്ടോമേറ്റ്"
desc = "PDF പ്രവർത്തനങ്ങൾ ബന്ധിപ്പിച്ച് മൾട്ടി-സ്റ്റെപ്പ് വർക്‌ഫ്ലോകൾ നിർമ്മിക്കുക. ആവർത്തിക്കുന്ന ജോലികൾക്ക് അനുയോജ്യം."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "മറ്റൊരു PDF-ന് മുകളിൽ PDF-കൾ ഓവർലേ ചെയ്യുന്നു"
title = "PDF-കൾ ഓവർലേ ചെയ്യുക"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF ടെക്സ്റ്റ് എഡിറ്റർ"
desc = "ഗ്രൂപ്പുചെയ്ത ടെക്സ്റ്റ് എഡിറ്റിംഗിനോടും PDF വീണ്ടും സൃഷ്ടിക്കുന്നതോടും കൂടി Stirling PDF JSON എക്സ്പോർട്ടുകൾ റിവ്യൂ ചെയ്ത് എഡിറ്റ് ചെയ്യുക"
@@ -4526,6 +4533,13 @@ description = "അക്കൗണ്ട് ലോക്കാകുന്നത
label = "ലോഗിൻ റീസെറ്റ് സമയം (മിനിറ്റ്)"
description = "പരാജയപ്പെട്ട ലോഗിൻ ശ്രമങ്ങൾ റീസെറ്റ് ചെയ്യുന്നതിന് മുമ്പുള്ള സമയം"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF സംരക്ഷണം അപ്രാപ്തമാക്കുക"
description = "Cross-Site Request Forgery സംരക്ഷണം അപ്രാപ്തമാക്കുക (ശുപാർശ ചെയ്യുന്നതല്ല)"
@@ -950,6 +950,7 @@ title = "Automatisch splitsen op grootte/aantal"
desc = "Splits een enkele PDF in meerdere documenten op basis van grootte, aantal pagina's of aantal documenten"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Kleur vervangen en inverteren"
desc = "Kleuren in PDF-documenten vervangen of inverteren"
@@ -964,18 +965,22 @@ title = "Geautomatiseerd mappenscannen"
desc = "Link naar handleiding voor geautomatiseerd mappenscannen"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO-gids"
desc = "Link naar SSO-gids"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped-installatie"
desc = "Link naar handleiding voor air-gapped-installatie"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Wachtwoord toevoegen"
desc = "Versleutel uw PDF-document met een wachtwoord."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Rechten wijzigen"
desc = "Documentbeperkingen en machtigingen wijzigen"
@@ -985,10 +990,12 @@ title = "Automatiseren"
desc = "Bouw workflows met meerdere stappen door PDF-acties te koppelen. Ideaal voor terugkerende taken."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Plaatst PDF's over een andere PDF heen"
title = "PDF's overlappen"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF-teksteditor"
desc = "Bewerk bestaande tekst en afbeeldingen in PDF's"
@@ -4526,6 +4533,13 @@ description = "Maximaal aantal mislukte inlogpogingen voordat het account wordt
label = "Resetperiode voor inloggen (minuten)"
description = "Tijd voordat mislukte inlogpogingen worden gereset"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF-bescherming uitschakelen"
description = "Cross-Site Request Forgery-bescherming uitschakelen (niet aanbevolen)"
@@ -950,6 +950,7 @@ title = "Auto-del størrelse/antall"
desc = "Del en enkelt PDF i flere dokumenter basert på størrelse, antall sider eller dokumenter"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Erstatt og inverter farger"
desc = "Erstatt eller inverter farger i PDF-dokumenter"
@@ -964,18 +965,22 @@ title = "Automatisert mappeskanning"
desc = "Lenke til veiledning for automatisert mappeskanning"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO-veiledning"
desc = "Lenke til SSO-veiledning"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Luftgap-oppsett"
desc = "Lenke til veiledning for luftgap-oppsett"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Legg til Passord"
desc = "Krypter din PDF-dokument med et passord."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Endre tillatelser"
desc = "Endre dokumentbegrensninger og tillatelser"
@@ -985,10 +990,12 @@ title = "Automatiser"
desc = "Bygg flertrinns arbeidsflyter ved å lenke sammen PDF-handlinger. Ideelt for gjentakende oppgaver."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Legger PDF-er over hverandre"
title = "Overlay PDF-er"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF-tekstredigerer"
desc = "Gå gjennom og rediger Stirling PDF JSON-eksporter med gruppert tekstredigering og regenerering av PDF"
@@ -4526,6 +4533,13 @@ description = "Maksimalt antall mislykkede innloggingsforsøk før kontolåsing"
label = "Tilbakestillingstid for innlogging (minutter)"
description = "Tid før mislykkede innloggingsforsøk nullstilles"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Deaktiver CSRF-beskyttelse"
description = "Deaktiver Cross-Site Request Forgery-beskyttelse (anbefales ikke)"
@@ -950,6 +950,7 @@ title = "Podziel (Rozmiar/Ilość stron)"
desc = "Rozdziela dokument PDF na wiele dokumentów bazując na podanym rozmiarze, ilości stron bądź ilości dokumentów"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Zastąp i odwróć kolor"
desc = "Zastępuj lub odwracaj kolory w dokumentach PDF"
@@ -964,18 +965,22 @@ title = "Auto skan folderów"
desc = "Link do przewodnika automatycznego skanowania folderów"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Przewodnik SSO"
desc = "Link do przewodnika SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Konfiguracja odizolowana"
desc = "Link do przewodnika konfiguracji odizolowanej"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Dodaj hasło"
desc = "Zaszyfruj dokument PDF za pomocą hasła."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Zmień uprawnienia"
desc = "Zmień ograniczenia i uprawnienia dokumentu"
@@ -985,10 +990,12 @@ title = "Automatyzuj"
desc = "Buduj wieloetapowe przepływy, łącząc akcje PDF. Idealne do powtarzających się zadań."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Nakłada dokumenty PDF na siebie"
title = "Nałóż PDFa"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Edytor tekstu PDF"
desc = "Przeglądaj i edytuj eksporty JSON z Stirling PDF z grupową edycją tekstu i ponowną generacją PDF"
@@ -4526,6 +4533,13 @@ description = "Maksymalna liczba nieudanych prób logowania przed zablokowaniem
label = "Czas resetu prób logowania (minuty)"
description = "Czas, po którym licznik nieudanych prób logowania jest resetowany"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Wyłącz ochronę CSRF"
description = "Wyłącz ochronę przed Cross-Site Request Forgery (niezalecane)"
@@ -950,6 +950,7 @@ title = "Divisão Manual do PDF"
desc = "Divida um PDF em vários, com base no tamanho, contagem de páginas ou contagem de documentos."
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Substituir e inverter cor"
desc = "Substituir ou inverter cores em documentos PDF"
@@ -964,18 +965,22 @@ title = "Varredura automática de pasta"
desc = "Link para o guia de varredura automática de pastas"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guia de SSO"
desc = "Link para o guia de SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Configuração Air-gapped"
desc = "Link para o guia de configuração isolada (air-gapped)"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Proteger PDF"
desc = "Criptografar seu PDF com uma senha podendo realizar alterações de permissões."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Alterar Permissões"
desc = "Alterar restrições e permissões do documento"
@@ -985,10 +990,12 @@ title = "Automatizar"
desc = "Crie fluxos de trabalho de várias etapas encadeando ações de PDF. Ideal para tarefas recorrentes."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Sobrepor um PDF sobre outro"
title = "Sobrepor PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor de texto de PDF"
desc = "Revise e edite exportações JSON do Stirling PDF com edição de texto agrupada e regeneração do PDF"
@@ -4526,6 +4533,13 @@ description = "Número máximo de tentativas de login com falha antes do bloquei
label = "Tempo para redefinir tentativas (minutos)"
description = "Tempo antes que as tentativas de login com falha sejam redefinidas"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Desativar proteção CSRF"
description = "Desativar a proteção contra Cross-Site Request Forgery (não recomendado)"
@@ -950,6 +950,7 @@ title = "Auto-dividir por tamanho/n.º"
desc = "Dividir um único PDF em múltiplos documentos baseado em tamanho, contagem de páginas, ou contagem de documentos"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Substituir e inverter cor"
desc = "Substituir ou inverter cores em documentos PDF"
@@ -964,18 +965,22 @@ title = "Varrimento auto de pastas"
desc = "Ligação para o guia de varrimento automático de pastas"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Guia de SSO"
desc = "Ligação para o guia de SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Configuração Air-gapped"
desc = "Ligação para o guia de configuração Air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Adicionar Palavra-passe"
desc = "Encriptar o seu documento PDF com uma palavra-passe."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Alterar Permissões"
desc = "Alterar restrições e permissões do documento"
@@ -985,10 +990,12 @@ title = "Automatizar"
desc = "Crie fluxos de trabalho de vários passos encadeando ações de PDF. Ideal para tarefas recorrentes."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Sobrepõe PDFs em cima de outro PDF"
title = "Sobrepor PDFs"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor de texto PDF"
desc = "Revise e edite exportações JSON do Stirling PDF com edição de texto agrupado e regeneração de PDF"
@@ -4526,6 +4533,13 @@ description = "Número máximo de tentativas falhadas de início de sessão ante
label = "Tempo de reposição do início de sessão (minutos)"
description = "Tempo até que as tentativas falhadas de início de sessão sejam repostas"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Desativar proteção CSRF"
description = "Desativar proteção Cross-Site Request Forgery (não recomendado)"
@@ -950,6 +950,7 @@ title = "Auto-împărțire mărime/pagini"
desc = "Împarte un singur PDF în mai multe documente bazat pe dimensiune, număr de pagini sau număr de documente"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Schimbă și inversează culori"
desc = "Înlocuiți sau inversați culorile în documente PDF"
@@ -964,18 +965,22 @@ title = "Scanare automată foldere"
desc = "Link către ghidul de scanare automată a folderelor"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Ghid SSO"
desc = "Link către ghidul SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Configurare air-gapped"
desc = "Link către ghidul de configurare izolată de rețea"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Adaugă Parolă"
desc = "Criptează documentul PDF cu o parolă."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Schimbă Permisiunile"
desc = "Schimbați restricțiile și permisiunile documentului"
@@ -985,10 +990,12 @@ title = "Automatizare"
desc = "Construiți fluxuri cu mai mulți pași legând acțiuni PDF. Ideal pentru sarcini recurente."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Suprapune PDF-uri peste alt PDF"
title = "Suprapune PDF-uri"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor text PDF"
desc = "Revizuiește și editează exporturile JSON Stirling PDF cu editare de text grupată și regenerare PDF"
@@ -4526,6 +4533,13 @@ description = "Numărul maxim de încercări eșuate înainte de blocarea contul
label = "Timp resetare autentificare (minute)"
description = "Timpul înainte de resetarea încercărilor eșuate"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Dezactivează protecția CSRF"
description = "Dezactivează protecția Cross-Site Request Forgery (nerecomandat)"
@@ -950,6 +950,7 @@ title = "Авторазбить по размеру/стр."
desc = "Разделяет один PDF на несколько документов на основе размера, количества страниц или количества документов"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Заменить и инвертировать цвет"
desc = "Заменяйте или инвертируйте цвета в PDF-документах"
@@ -964,18 +965,22 @@ title = "Автосканирование папок"
desc = "Ссылка на руководство по автоматическому сканированию папок"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Руководство по SSO"
desc = "Ссылка на руководство по SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Изолированная установка"
desc = "Ссылка на руководство по изолированной установке"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Добавить пароль"
desc = "Зашифруйте ваш PDF-документ паролем."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Изменить разрешения"
desc = "Изменение ограничений и разрешений документа"
@@ -985,10 +990,12 @@ title = "Автоматизация"
desc = "Создавайте многошаговые процессы, связывая PDF-действия. Идеально для повторяющихся задач."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Наложить один PDF поверх другого"
title = "Наложение PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Редактор текста в PDF"
desc = "Просмотр и редактирование экспортов Stirling PDF в JSON с групповым редактированием текста и регенерацией PDF"
@@ -4526,6 +4533,13 @@ description = "Максимальное число неудачных попыт
label = "Время сброса попыток (минуты)"
description = "Время до сброса счетчика неудачных попыток входа"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Отключить защиту CSRF"
description = "Отключить защиту от межсайтовой подделки запросов (не рекомендуется)"
@@ -950,6 +950,7 @@ title = "Auto rozdeliť veľkosť/počet"
desc = "Rozdelí jeden PDF na viacero dokumentov na základe veľkosti, počtu stránok alebo počtu dokumentov"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Nahradiť a invertovať farby"
desc = "Nahradiť alebo invertovať farby v dokumentoch PDF"
@@ -964,18 +965,22 @@ title = "Auto skenovanie priečinkov"
desc = "Odkaz na príručku k automatizovanému skenovaniu priečinkov"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO príručka"
desc = "Odkaz na SSO príručku"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped nastavenie"
desc = "Odkaz na príručku k air-gapped nastaveniu"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Pridať heslo"
desc = "Šifrovať váš PDF dokument heslom."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Zmeniť povolenia"
desc = "Zmeniť obmedzenia a povolenia dokumentu"
@@ -985,10 +990,12 @@ title = "Automatizovať"
desc = "Stavať viacstupňové pracovné postupy spájaním akcií PDF. Ideálne pre opakované úlohy."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Prekrýva PDF súbory na iný PDF"
title = "Prekrývanie PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Editor textu PDF"
desc = "Kontrolujte a upravujte Stirling PDF JSON exporty so skupinovými úpravami textu a opätovným vytvorením PDF"
@@ -4526,6 +4533,13 @@ description = "Maximálny počet neúspešných pokusov o prihlásenie pred zabl
label = "Reset prihlásenia (minúty)"
description = "Čas, po ktorom sa zlyhané pokusy o prihlásenie vynulujú"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Zakázať ochranu CSRF"
description = "Zakázať ochranu proti Cross-Site Request Forgery (neodporúča sa)"
@@ -950,6 +950,7 @@ title = "Samodejno razdeli po vel./št."
desc = "Razdeli en PDF na več dokumentov glede na velikost, število strani ali število dokumentov"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Zamenjaj in invertiraj barve"
desc = "Zamenjajte ali invertirajte barve v dokumentih PDF"
@@ -964,18 +965,22 @@ title = "Samodejno skeniranje map"
desc = "Povezava do vodiča za avtomatizirano skeniranje map"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Vodič za SSO"
desc = "Povezava do vodiča za SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Izolirana namestitev"
desc = "Povezava do vodiča za namestitev v izoliranem omrežju"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Dodaj geslo"
desc = "Šifrirajte svoj dokument PDF z geslom."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Spremeni dovoljenja"
desc = "Spremenite omejitve in dovoljenja dokumenta"
@@ -985,10 +990,12 @@ title = "Avtomatiziraj"
desc = "Sestavite večkorakovne poteke z veriženjem dejanj PDF. Idealno za ponavljajoče se naloge."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Prekriva PDF-je na vrhu drugega PDF-ja"
title = "Prekrivanje PDF-jev"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Urejevalnik besedila PDF"
desc = "Pregledujte in urejajte Stirling PDF JSON izvoze z urejanjem združenega besedila in ponovnim ustvarjanjem PDF"
@@ -4526,6 +4533,13 @@ description = "Največje število neuspelih poskusov prijave pred zaklepom raču
label = "Čas ponastavitve prijave (minute)"
description = "Čas do ponastavitve neuspelih poskusov prijave"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Onemogoči zaščito CSRF"
description = "Onemogoči zaščito pred ponarejanjem zahtev (ni priporočljivo)"
@@ -950,6 +950,7 @@ title = "Automatsko deljenje po veličini/broju"
desc = "Deljenje jednog PDF-a na više na osnovu veličine, broja stranica ili broja dokumenata"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Zameni i invertiraj boju"
desc = "Zamenite ili invertirajte boje u PDF dokumentima"
@@ -964,18 +965,22 @@ title = "Automatsko skeniranje fascikli"
desc = "Link ka vodiču za automatsko skeniranje fascikli"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO vodič"
desc = "Link ka SSO vodiču"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Air-gapped podešavanje"
desc = "Link ka vodiču za air-gapped podešavanje"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Dodaj lozinku"
desc = "Enkriptujte vaš PDF dokument lozinkom."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Promeni dozvole"
desc = "Promenite ograničenja dokumenta i dozvole"
@@ -985,10 +990,12 @@ title = "Automatizuj"
desc = "Gradite višekorake tokove rada povezivanjem PDF akcija. Idealno za ponavljajuće zadatke."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Preklapa PDF-ove jedan preko drugog"
title = "Preklapanje PDF-ova"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF uređivač teksta"
desc = "Pregledajte i uređujte Stirling PDF JSON izvoze uz grupisano uređivanje teksta i ponovno generisanje PDF-a"
@@ -4526,6 +4533,13 @@ description = "Maksimalan broj neuspelih pokušaja prijave pre zaključavanja na
label = "Vreme resetovanja (minute)"
description = "Vreme posle kog se neuspešni pokušaji prijave resetuju"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Onemogući CSRF zaštitu"
description = "Onemogući Cross-Site Request Forgery zaštitu (nije preporučeno)"
@@ -950,6 +950,7 @@ title = "Auto-dela efter storlek/antal"
desc = "Dela en enda PDF till flera dokument baserat på storlek, sidantal eller dokumentantal"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Ersätt och invertera färg"
desc = "Ersätt eller invertera färger i PDF‑dokument"
@@ -964,18 +965,22 @@ title = "Automatiserad mappskanning"
desc = "Länk till guide för automatiserad mappskanning"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO‑guide"
desc = "Länk till SSO‑guide"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Isolerad installation"
desc = "Länk till guide för isolerad installation"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Lägg till lösenord"
desc = "Kryptera ditt PDF-dokument med ett lösenord."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Ändra behörigheter"
desc = "Ändra dokumentrestriktioner och behörigheter"
@@ -985,10 +990,12 @@ title = "Automatisera"
desc = "Skapa flerstegade arbetsflöden genom att kedja ihop PDF‑åtgärder. Perfekt för återkommande uppgifter."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Överlagrar PDF:er ovanpå en annan PDF"
title = "Överlagra PDF:er"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF-textredigerare"
desc = "Granska och redigera Stirling PDF JSON-exporter med grupperad textredigering och PDF-återgenerering"
@@ -4526,6 +4533,13 @@ description = "Maximalt antal misslyckade inloggningsförsök innan kontot låse
label = "Återställningstid för inloggning (minuter)"
description = "Tid innan misslyckade inloggningsförsök nollställs"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Inaktivera CSRF-skydd"
description = "Inaktivera skydd mot Cross-Site Request Forgery (rekommenderas inte)"
@@ -950,6 +950,7 @@ title = "แยกตามขนาด/จำนวน"
desc = "แยก PDF เป็นเอกสารหลายฉบับตามขนาด จำนวนหน้า หรือจำนวนเอกสาร"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "แทนที่และกลับสี"
desc = "แทนที่หรือกลับสีในเอกสาร PDF"
@@ -964,18 +965,22 @@ title = "สแกน โฟลเดอร์ อัตโนมัติ"
desc = "ลิงก์ไปยังคู่มือการสแกนโฟลเดอร์อัตโนมัติ"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "คู่มือ SSO"
desc = "ลิงก์ไปยังคู่มือ SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "การตั้งค่า Air-gapped"
desc = "ลิงก์ไปยังคู่มือการตั้งค่า Air-gapped"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "เพิ่มรหัสผ่าน"
desc = "เข้ารหัสเอกสาร PDF ของคุณด้วยรหัสผ่าน"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "เปลี่ยนสิทธิ์"
desc = "เปลี่ยนข้อจำกัดและสิทธิ์ของเอกสาร"
@@ -985,10 +990,12 @@ title = "ทำให้อัตโนมัติ"
desc = "สร้างเวิร์กโฟลว์หลายขั้นตอนโดยเชื่อมโยงการกระทำ PDF เข้าด้วยกัน เหมาะสำหรับงานที่เกิดซ้ำ"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "ซ้อนทับ PDF บน PDF อีกไฟล์หนึ่ง"
title = "ซ้อนทับ PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "แก้ไขข้อความ PDF"
desc = "ตรวจทานและแก้ไขไฟล์ JSON ที่ส่งออกจาก Stirling PDF ด้วยการแก้ไขข้อความแบบกลุ่มและการสร้าง PDF ใหม่"
@@ -4526,6 +4533,13 @@ description = "จำนวนครั้งสูงสุดของกา
label = "เวลาล้างจำนวนครั้ง (นาที)"
description = "ระยะเวลาก่อนที่จะรีเซ็ตจำนวนครั้งที่เข้าสู่ระบบล้มเหลว"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "ปิดการป้องกัน CSRF"
description = "ปิดการป้องกัน Cross-Site Request Forgery (ไม่แนะนำ)"
@@ -950,6 +950,7 @@ title = "Otomatik Boyut/Sayıya Böl"
desc = "Tek bir PDF'yi boyut, sayfa sayısı veya belge sayısına göre birden fazla belgeye bölün"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Rengi Değiştir & Ters Çevir"
desc = "PDF belgelerindeki renkleri değiştirin veya tersine çevirin"
@@ -964,18 +965,22 @@ title = "Otomatik Klasör Tarama"
desc = "Otomatik klasör tarama kılavuzuna bağlantı"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO Kılavuzu"
desc = "SSO kılavuzuna bağlantı"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Ağdan İzole Kurulum"
desc = "Ağdan izole kurulum kılavuzuna bağlantı"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Parola Ekle"
desc = "PDF belgenizi bir parola ile şifreleyin."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "İzinleri Değiştir"
desc = "Belge kısıtlamalarını ve izinleri değiştirin"
@@ -985,10 +990,12 @@ title = "Otomatikleştir"
desc = "PDF eylemlerini birbirine bağlayarak çok adımlı iş akışları oluşturun. Tekrarlayan görevler için idealdir."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF'leri başka bir PDF'nin üzerine bindirir"
title = "PDF'leri Bindirme"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF Metin Düzenleyici"
desc = "Gruplu metin düzenleme ve PDF yeniden oluşturma ile Stirling PDF JSON dışa aktarımlarını gözden geçirin ve düzenleyin"
@@ -4526,6 +4533,13 @@ description = "Hesap kilitlenmeden önceki maksimum başarısız oturum açma de
label = "Oturum Açma Sıfırlama Süresi (dakika)"
description = "Başarısız oturum açma denemelerinin sıfırlanacağı süre"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "CSRF Korumasını Devre Dışı Bırak"
description = "Siteler Arası İstek Sahteciliği (CSRF) korumasını devre dışı bırak (önerilmez)"
@@ -950,6 +950,7 @@ title = "Автоподіл за розміром/стор."
desc = "Розділяє один PDF на кілька документів на основі розміру, кількості сторінок або кількості документів"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Замінити й інвертувати колір"
desc = "Замінювати або інвертувати кольори в PDF-документах"
@@ -964,18 +965,22 @@ title = "Автоматичне сканування папок"
desc = "Посилання на посібник із автоматичного сканування папок"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Посібник з SSO"
desc = "Посилання на посібник з SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Ізольоване розгортання"
desc = "Посилання на посібник з налаштування ізольованого середовища"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Додати пароль"
desc = "Зашифруйте документ PDF паролем."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Змінити дозволи"
desc = "Змінити обмеження та дозволи документа"
@@ -985,10 +990,12 @@ title = "Автоматизація"
desc = "Створюйте багатокрокові робочі процеси, поєднуючи дії з PDF. Ідеально для повторюваних завдань."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Накладення одного PDF поверх іншого PDF"
title = "Накладення PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Редактор тексту PDF"
desc = "Переглядайте й редагуйте JSON-експорти Stirling PDF з груповим редагуванням тексту та повторною генерацією PDF"
@@ -4526,6 +4533,13 @@ description = "Максимальна кількість невдалих спр
label = "Час скидання спроб (хвилини)"
description = "Час, після якого лічильник невдалих спроб входу скидається"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Вимкнути захист CSRF"
description = "Вимкнути захист від Cross-Site Request Forgery (не рекомендовано)"
@@ -950,6 +950,7 @@ title = "Tự động chia theo kích thước/số lượng"
desc = "Chia một tệp PDF thành nhiều tài liệu dựa trên kích thước, số trang hoặc số lượng tài liệu"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "Thay thế & Đảo ngược Màu"
desc = "Thay thế hoặc đảo ngược màu trong tài liệu PDF"
@@ -964,18 +965,22 @@ title = "Quét thư mục tự động"
desc = "Liên kết tới hướng dẫn quét thư mục tự động"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "Hướng dẫn SSO"
desc = "Liên kết tới hướng dẫn SSO"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "Thiết lập cách ly mạng"
desc = "Liên kết tới hướng dẫn thiết lập cách ly mạng"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "Thêm mật khẩu"
desc = "Mã hóa tài liệu PDF của bạn bằng mật khẩu."
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "Thay đổi quyền"
desc = "Thay đổi hạn chế và quyền của tài liệu"
@@ -985,10 +990,12 @@ title = "Tự động hóa"
desc = "Xây dựng quy trình nhiều bước bằng cách xâu chuỗi các thao tác PDF. Lý tưởng cho các tác vụ lặp lại."
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "Chồng lớp PDF lên trên PDF khác"
title = "Chồng lớp PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "Trình chỉnh sửa văn bản PDF"
desc = "Xem và chỉnh sửa xuất JSON của Stirling PDF với chỉnh sửa văn bản theo nhóm và tái tạo PDF"
@@ -4526,6 +4533,13 @@ description = "Số lần đăng nhập thất bại tối đa trước khi khó
label = "Thời gian đặt lại đăng nhập (phút)"
description = "Thời gian trước khi đặt lại số lần đăng nhập thất bại"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "Tắt bảo vệ CSRF"
description = "Tắt bảo vệ Cross-Site Request Forgery (không khuyến nghị)"
@@ -950,6 +950,7 @@ title = "按大小/页数自动拆分"
desc = "按文件大小或页数自动拆分 PDF"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "替换与反转颜色"
desc = "替换或反转 PDF 文档中的颜色"
@@ -964,18 +965,22 @@ title = "自动化文件夹扫描"
desc = "链接到自动化文件夹扫描指南"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO 指南"
desc = "链接到 SSO 指南"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "离线隔离部署"
desc = "链接到离线隔离部署指南"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "གསང་ཚིག་སྣོན་པ།"
desc = "PDF ཡིག་ཆར་གསང་ཚིག་གིས་གསང་སྡོམ་བྱེད་པ།"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "更改权限"
desc = "更改文档限制与权限"
@@ -985,10 +990,12 @@ title = "自动化"
desc = "通过串联 PDF 动作构建多步工作流。适合重复性任务。"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "PDF གཞན་ཞིག་གི་སྟེང་དུ་ PDF བརྩེགས་པ།"
title = "PDF སྟེང་བརྩེགས།"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF 文本编辑器"
desc = "审阅并编辑 Stirling PDF 的 JSON 导出,支持分组文本编辑与 PDF 再生成"
@@ -4526,6 +4533,13 @@ description = "在账户锁定前允许的最大失败登录次数"
label = "登录重置时间(分钟)"
description = "失败登录尝试被重置前的时间"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "禁用 CSRF 保护"
description = "禁用跨站请求伪造保护(不推荐)"
@@ -950,6 +950,7 @@ title = "自动根据大小/数目拆分 PDF"
desc = "将单个 PDF 拆分为多个文档,基于大小、页数或文档数"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "替换和反转颜色"
desc = "替换或反转 PDF 文档中的颜色"
@@ -964,18 +965,22 @@ title = "自动文件夹扫描"
desc = "跳转至自动文件夹扫描指南"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO 指南"
desc = "跳转至 SSO 指南"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "离线/隔离部署"
desc = "跳转至隔离部署指南"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "添加密码"
desc = "使用密码对 PDF 文档进行加密。"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "更改权限"
desc = "更改文档限制与权限"
@@ -985,10 +990,12 @@ title = "自动化"
desc = "通过串联 PDF 操作构建多步工作流。适合重复性任务。"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "将一个 PDF 叠加在另一个之上"
title = "叠加 PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF 文本编辑器"
desc = "审阅并编辑 Stirling PDF 导出的 JSON,支持分组文本编辑并重新生成 PDF"
@@ -4526,6 +4533,13 @@ description = "达到该失败次数后将锁定账户"
label = "登录重置时间(分钟)"
description = "重置失败尝试计数的时间"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "禁用CSRF保护"
description = "禁用跨站请求伪造保护(不建议)"
@@ -950,6 +950,7 @@ title = "根據大小/數量自動分割"
desc = "根據大小、頁數或文件數將單一 PDF 分割為多個文件"
[home.replaceColor]
tags = "replace color,invert color,color replacement,swap colors,change color,invert,negative,color swap,find and replace color,convert color,color change"
title = "取代與反轉顏色"
desc = "在 PDF 文件中取代或反轉顏色"
@@ -964,18 +965,22 @@ title = "自動化資料夾掃描"
desc = "連結至自動化資料夾掃描指南"
[home.devSsoGuide]
tags = "SSO,single sign-on,authentication,SAML,OAuth,OIDC,login,enterprise,identity provider,IdP"
title = "SSO 指南"
desc = "連結至 SSO 指南"
[home.devAirgapped]
tags = "air-gapped,offline,isolated,no internet,disconnected,secure,network isolation,standalone"
title = "隔離網路設定"
desc = "連結至隔離網路設定指南"
[home.addPassword]
tags = "encrypt,password,lock,secure,protect,security,encryption,safeguard,confidential,private,restrict access"
title = "新增密碼"
desc = "用密碼加密您的 PDF 檔案。"
[home.changePermissions]
tags = "permissions,restrictions,rights,access control,allow,deny,printing,copying,editing,modify permissions,security settings,user rights"
title = "變更權限"
desc = "變更文件限制與權限"
@@ -985,10 +990,12 @@ title = "自動化"
desc = "將多個 PDF 動作串接,建立多步驟工作流程。適合重複性工作。"
[home.overlay-pdfs]
tags = "overlay,combine,merge,layer,superimpose,overlay PDF,layer PDFs,combine PDFs,stack,overlay pages,background,foreground,composite"
desc = "將 PDF 覆蓋在另一個 PDF 上"
title = "覆蓋 PDF"
[home.pdfTextEditor]
tags = "edit text,modify text,change text,edit content,update text,rewrite,correct,amend,redline,revise,text editor,content editor"
title = "PDF 文字編輯器"
desc = "檢視與編輯 Stirling PDF 的 JSON 匯出,支援群組文字編輯與重新產生 PDF"
@@ -4526,6 +4533,13 @@ description = "帳戶被鎖定前允許的最大登入失敗次數"
label = "登入重設時間(分鐘)"
description = "登入失敗次數重設所需的時間"
[admin.settings.security.xFrameOptions]
label = "X-Frame-Options"
description = "Controls whether the application can be embedded in iframes"
deny = "Deny (Prevents all framing)"
sameorigin = "Same Origin (Allow framing from same domain)"
disabled = "Disabled (No X-Frame-Options header)"
[admin.settings.security.csrfDisabled]
label = "停用 CSRF 保護"
description = "停用跨站請求偽造保護(不建議)"
+601 -685
View File
File diff suppressed because it is too large Load Diff
+10 -10
View File
@@ -18,24 +18,24 @@ name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
tauri-build = { version = "2.5.3", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.9.0", features = [ "devtools"] }
tauri-plugin-log = "2.0.0-rc"
tauri-plugin-shell = "2.1.0"
tauri-plugin-fs = "2.4.4"
tauri-plugin-http = { version = "2.4.4", features = ["dangerous-settings"] }
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
tauri-plugin-store = "2.1.0"
tauri-plugin-opener = "2.0.0"
tauri-plugin-deep-link = "2.4.5"
tauri-plugin-log = "2.8.0"
tauri-plugin-shell = "2.3.4"
tauri-plugin-fs = "2.4.5"
tauri-plugin-http = { version = "2.5.6", features = ["dangerous-settings"] }
tauri-plugin-single-instance = { version = "2.3.7", features = ["deep-link"] }
tauri-plugin-store = "2.4.2"
tauri-plugin-opener = "2.5.3"
tauri-plugin-deep-link = "2.4.6"
keyring = { version = "3.6.1", features = ["apple-native", "windows-native"] }
tokio = { version = "1.0", features = ["time", "sync"] }
reqwest = { version = "0.11", features = ["json"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "rustls-tls-native-roots"] }
tiny_http = "0.12"
url = "2.5"
urlencoding = "2.1"
+97 -6
View File
@@ -213,8 +213,20 @@ pub async fn login(
// Detect if this is Supabase (SaaS) or Spring Boot (self-hosted)
let is_supabase = server_url.trim_end_matches('/') == saas_server_url.trim_end_matches('/');
// Create HTTP client
let client = reqwest::Client::new();
// Create HTTP client with certificate bypass
// This handles:
// - Self-signed certificates
// - Missing intermediate certificates
// - Certificate hostname mismatches
// Note: Rustls only supports TLS 1.2 and TLS 1.3
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| {
log::error!("Failed to create HTTP client: {}", e);
format!("Failed to create HTTP client: {}", e)
})?;
if is_supabase {
// Supabase authentication flow
@@ -235,7 +247,24 @@ pub async fn login(
.json(&request_body)
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
.map_err(|e| {
let error_msg = e.to_string();
let error_lower = error_msg.to_lowercase();
log::error!("Supabase login network error: {}", e);
// Detect TLS version mismatch
if error_lower.contains("peer is incompatible") ||
error_lower.contains("protocol version") ||
error_lower.contains("peerincompatible") ||
(error_lower.contains("handshake") && (error_lower.contains("tls") || error_lower.contains("ssl"))) {
format!(
"TLS version not supported: The Supabase server appears to require an unsupported TLS version. \
Please contact support. Technical details: {}", e
)
} else {
format!("Network error connecting to Supabase: {}", e)
}
})?;
let status = response.status();
@@ -296,7 +325,39 @@ pub async fn login(
.json(&payload)
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
.map_err(|e| {
let error_msg = e.to_string();
let error_lower = error_msg.to_lowercase();
log::error!("Spring Boot login network error: {}", e);
// Detect TLS version mismatch (server using TLS 1.0/1.1)
if error_lower.contains("peer is incompatible") ||
error_lower.contains("protocol version") ||
error_lower.contains("peerincompatible") ||
(error_lower.contains("handshake") && (error_lower.contains("tls") || error_lower.contains("ssl"))) {
format!(
"TLS version not supported: The server appears to be using TLS 1.0 or TLS 1.1, which are not supported by this desktop app. \
Please upgrade your server to use TLS 1.2 or higher, or use the web version of Stirling-PDF instead. \
Technical details: {}", e
)
// Other TLS/SSL errors (certificate issues)
} else if error_lower.contains("tls") || error_lower.contains("ssl") ||
error_lower.contains("certificate") || error_lower.contains("decrypt") {
format!(
"TLS/SSL connection error: This usually means the server has certificate issues. \
The desktop app accepts self-signed certificates, so this might be a TLS version issue. \
Technical details: {}", e
)
} else if error_lower.contains("connection refused") {
format!("Connection refused: Server is not reachable at {}. Check if the server is running and the URL is correct.", login_url)
} else if error_lower.contains("timeout") {
format!("Connection timeout: Server at {} is not responding. Check your network connection.", login_url)
} else if error_lower.contains("dns") || error_lower.contains("resolve") {
format!("DNS resolution failed: Cannot resolve hostname. Check if the server URL is correct.")
} else {
format!("Network error: {}", e)
}
})?;
let status = response.status();
log::debug!("Spring Boot login response status: {}", status);
@@ -505,7 +566,20 @@ async fn exchange_code_for_token(
) -> Result<OAuthCallbackResult, String> {
log::info!("Exchanging authorization code for access token with PKCE");
let client = reqwest::Client::new();
// Create HTTP client with certificate bypass
// This handles:
// - Self-signed certificates
// - Missing intermediate certificates
// - Certificate hostname mismatches
// Note: Rustls only supports TLS 1.2 and TLS 1.3
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(30))
.build()
.map_err(|e| {
log::error!("Failed to create HTTP client: {}", e);
format!("Failed to create HTTP client: {}", e)
})?;
// grant_type goes in query string, not body!
let token_url = format!("{}/auth/v1/token?grant_type=pkce", auth_server_url.trim_end_matches('/'));
@@ -526,7 +600,24 @@ async fn exchange_code_for_token(
.json(&body)
.send()
.await
.map_err(|e| format!("Failed to exchange code for token: {}", e))?;
.map_err(|e| {
let error_msg = e.to_string();
let error_lower = error_msg.to_lowercase();
log::error!("OAuth token exchange network error: {}", e);
// Detect TLS version mismatch
if error_lower.contains("peer is incompatible") ||
error_lower.contains("protocol version") ||
error_lower.contains("peerincompatible") ||
(error_lower.contains("handshake") && (error_lower.contains("tls") || error_lower.contains("ssl"))) {
format!(
"TLS version not supported: The authentication server appears to require an unsupported TLS version. \
Please contact support. Technical details: {}", e
)
} else {
format!("Failed to exchange code for token: {}", e)
}
})?;
let status = response.status();
if !status.is_success() {
@@ -11,6 +11,8 @@ pub enum ConnectionMode {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub url: String,
#[serde(default)]
pub allow_invalid_certs: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
+3 -2
View File
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "Stirling-PDF",
"version": "2.4.0",
"version": "2.4.1",
"identifier": "stirling.pdf.dev",
"build": {
"frontendDist": "../dist",
@@ -16,7 +16,8 @@
"width": 1280,
"height": 800,
"resizable": true,
"fullscreen": false
"fullscreen": false,
"additionalBrowserArgs": "--enable-features=CertVerifierBuiltinFeature"
}
]
},
@@ -10,6 +10,7 @@ import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
import { Z_INDEX_OVER_FILE_MANAGER_MODAL } from '@app/styles/zIndex';
import { withBasePath } from '@app/constants/app';
import { convertImageToPdf, isImageFile } from '@app/utils/imageToPdfUtils';
import apiClient from '@app/services/apiClient';
interface MobileUploadModalProps {
opened: boolean;
@@ -75,26 +76,27 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
// Use configured frontendUrl if set, otherwise use current origin
// Combine with base path and mobile-scanner route
const frontendUrl = config?.frontendUrl || window.location.origin;
const baseUrl = localStorage.getItem('server_url') || '';
const frontendUrl = baseUrl || config?.frontendUrl || window.location.origin;
const mobileUrl = `${frontendUrl}${withBasePath('/mobile-scanner')}?session=${sessionId}`;
// Create session on backend
const createSession = useCallback(async (newSessionId: string) => {
try {
const response = await fetch(`/api/v1/mobile-scanner/create-session/${newSessionId}`, {
method: 'POST'
const response = await apiClient.post<SessionInfo>(`/api/v1/mobile-scanner/create-session/${newSessionId}`, undefined, {
responseType: 'json',
});
if (!response.ok) {
if (!response.status || response.status !== 200) {
throw new Error('Failed to create session');
}
const data = await response.json();
const data = response.data;
setSessionInfo(data);
setError(null);
console.log('Session created:', data);
console.log('[MobileUploadModal] Session created:', data);
} catch (err) {
console.error('Failed to create session:', err);
console.error('[MobileUploadModal] Failed to create session:', err);
setError(t('mobileUpload.sessionCreateError', 'Failed to create session'));
}
}, [t]);
@@ -113,12 +115,12 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
if (!opened) return;
try {
const response = await fetch(`/api/v1/mobile-scanner/files/${sessionId}`);
if (!response.ok) {
const response = await apiClient.get(`/api/v1/mobile-scanner/files/${sessionId}`);
if (!response.status || response.status !== 200) {
throw new Error('Failed to check for files');
}
const data = await response.json();
const data = response.data;
const files = data.files || [];
// Download only files we haven't processed yet
@@ -127,12 +129,14 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
if (newFiles.length > 0) {
for (const fileMetadata of newFiles) {
try {
const downloadResponse = await fetch(
`/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`
const downloadResponse = await apiClient.get(
`/api/v1/mobile-scanner/download/${sessionId}/${fileMetadata.filename}`, {
responseType: 'blob',
}
);
if (downloadResponse.ok) {
const blob = await downloadResponse.blob();
if (downloadResponse.status === 200) {
const blob = downloadResponse.data;
let file = new File([blob], fileMetadata.filename, {
type: fileMetadata.contentType || 'image/jpeg'
});
@@ -145,9 +149,9 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
pageFormat: config?.mobileScannerPageFormat as 'keep' | 'A4' | 'letter' | undefined,
stretchToFit: config?.mobileScannerStretchToFit,
});
console.log('Converted image to PDF:', file.name);
console.log('[MobileUploadModal] Converted image to PDF:', file.name);
} catch (convertError) {
console.warn('Failed to convert image to PDF, using original file:', convertError);
console.warn('[MobileUploadModal] Failed to convert image to PDF, using original file:', convertError);
// Continue with original image file if conversion fails
}
}
@@ -157,21 +161,21 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
onFilesReceived([file]);
}
} catch (err) {
console.error('Failed to download file:', fileMetadata.filename, err);
console.error('[MobileUploadModal] Failed to download file:', fileMetadata.filename, err);
}
}
// Delete the entire session immediately after downloading all files
// This ensures files are only on server for ~1 second
try {
await fetch(`/api/v1/mobile-scanner/session/${sessionId}`, { method: 'DELETE' });
console.log('Session cleaned up after file download');
await apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`);
console.log('[MobileUploadModal] Session cleaned up after file download');
} catch (cleanupErr) {
console.warn('Failed to cleanup session after download:', cleanupErr);
console.warn('[MobileUploadModal] Failed to cleanup session after download:', cleanupErr);
}
}
} catch (err) {
console.error('Error polling for files:', err);
console.error('[MobileUploadModal] Error polling for files:', err);
setError(t('mobileUpload.pollingError', 'Error checking for files'));
}
}, [opened, sessionId, onFilesReceived, t]);
@@ -184,14 +188,24 @@ export default function MobileUploadModal({ opened, onClose, onFilesReceived }:
setError(null);
setShowExpiryWarning(false);
processedFiles.current.clear();
} else {
// Clean up session when modal closes
if (sessionId) {
fetch(`/api/v1/mobile-scanner/session/${sessionId}`, { method: 'DELETE' })
.catch(err => console.warn('Failed to cleanup session on close:', err));
}
}
}, [opened]); // Only run when opened changes
}, [opened, sessionId]); // Only run when opened changes
useEffect(() => {
if (!opened) return;
createSession(sessionId);
setFilesReceived(0);
setError(null);
setShowExpiryWarning(false);
processedFiles.current.clear();
return () => {
console.log('Cleaning up session on unmount/close:', sessionId);
apiClient.delete(`/api/v1/mobile-scanner/session/${sessionId}`)
.catch(err => console.warn('[MobileUploadModal] Cleanup failed:', err));
};
}, [opened, sessionId, createSession]);
// Start polling for files when modal opens
useEffect(() => {
@@ -223,7 +223,7 @@ export default function AutomationCreation({ mode, existingAutomation, onBack, o
createdAt: existingAutomation?.createdAt || new Date().toISOString(),
updatedAt: new Date().toISOString()
};
downloadFolderScanningConfig(tempAutomation);
downloadFolderScanningConfig(tempAutomation, toolRegistry);
}}
disabled={!canSaveAutomation()}
variant="light"
@@ -59,7 +59,7 @@ export default function AutomationSelection({
onClick={() => onRun(automation)}
showMenu={true}
onEdit={() => onEdit(automation)}
onExport={() => downloadFolderScanningConfig(automation)}
onExport={() => downloadFolderScanningConfig(automation, toolRegistry)}
onDelete={() => onDelete(automation)}
toolRegistry={toolRegistry}
/>
@@ -0,0 +1,64 @@
import React, { createContext, useContext, useState, useEffect, useRef } from 'react';
import { useDocumentManagerPlugin } from '@embedpdf/plugin-document-manager/react';
interface ActiveDocumentContextType {
documentId: string | null;
}
const ActiveDocumentContext = createContext<ActiveDocumentContextType>({ documentId: null });
export function ActiveDocumentProvider({ children }: { children: React.ReactNode }) {
const { plugin, isLoading } = useDocumentManagerPlugin();
const [documentId, setDocumentId] = useState<string | null>(null);
const unsubscribeRef = useRef<(() => void) | null>(null);
const documentIdRef = useRef<string | null>(null);
useEffect(() => {
if (isLoading || !plugin) return;
const docManagerApi = plugin.provides?.();
if (!docManagerApi) return;
// Get initial active document (synchronously if available)
const activeDoc = docManagerApi.getActiveDocument?.();
if (activeDoc?.id && activeDoc.id !== documentIdRef.current) {
documentIdRef.current = activeDoc.id;
setDocumentId(activeDoc.id);
}
// Subscribe to document changes (only if not already subscribed)
if (!unsubscribeRef.current && docManagerApi.onDocumentOpened) {
unsubscribeRef.current = docManagerApi.onDocumentOpened((event: any) => {
const docId = event?.documentId || event?.id || event?.document?.id;
if (docId && docId !== documentIdRef.current) {
documentIdRef.current = docId;
setDocumentId(docId);
}
});
}
// Note: We don't unsubscribe on effect cleanup to avoid re-subscribing on every render
// Cleanup happens only on unmount
}, [plugin, isLoading]);
// Cleanup on unmount only
useEffect(() => {
return () => {
if (unsubscribeRef.current) {
unsubscribeRef.current();
unsubscribeRef.current = null;
}
};
}, []);
return (
<ActiveDocumentContext.Provider value={{ documentId }}>
{children}
</ActiveDocumentContext.Provider>
);
}
export function useActiveDocument(): string | null {
const context = useContext(ActiveDocumentContext);
return context.documentId;
}
@@ -10,6 +10,7 @@ import type {
} from '@app/components/viewer/viewerTypes';
type NoteIcon = NonNullable<AnnotationToolOptions['icon']>;
type AnnotationDefaults =
| {
type:
@@ -17,13 +18,15 @@ type AnnotationDefaults =
| PdfAnnotationSubtype.UNDERLINE
| PdfAnnotationSubtype.STRIKEOUT
| PdfAnnotationSubtype.SQUIGGLY;
color: string;
strokeColor: string;
color?: string;
opacity: number;
customData?: Record<string, unknown>;
}
| {
type: PdfAnnotationSubtype.INK;
color: string;
strokeColor: string;
color?: string;
opacity?: number;
borderWidth?: number;
strokeWidth?: number;
@@ -37,7 +40,8 @@ type AnnotationDefaults =
fontFamily?: string;
textAlign?: number;
opacity?: number;
backgroundColor?: string;
color?: string;
interiorColor?: string;
borderWidth?: number;
contents?: string;
icon?: PdfAnnotationIcon;
@@ -106,7 +110,7 @@ const DEFAULTS = {
ink: '#1f2933',
inkHighlighter: '#ffd54f',
text: '#111111',
note: '#ffd54f', // match highlight color
note: '#ffd54f',
shapeFill: '#0000ff',
shapeStroke: '#cf5b5b',
shapeOpacity: 0.5,
@@ -124,42 +128,62 @@ const buildStampDefaults: ToolDefaultsBuilder = (options) => ({
...withCustomData(options),
});
const buildInkDefaults = (options?: AnnotationToolOptions, opacityOverride?: number): AnnotationDefaults => ({
type: PdfAnnotationSubtype.INK,
color: options?.color ?? (opacityOverride ? DEFAULTS.inkHighlighter : DEFAULTS.ink),
opacity: options?.opacity ?? opacityOverride ?? 1,
borderWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
strokeWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
lineWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
...withCustomData(options),
});
const buildInkDefaults = (options?: AnnotationToolOptions, opacityOverride?: number): AnnotationDefaults => {
const colorValue = options?.color ?? (opacityOverride ? DEFAULTS.inkHighlighter : DEFAULTS.ink);
return {
type: PdfAnnotationSubtype.INK,
strokeColor: colorValue,
color: colorValue,
opacity: options?.opacity ?? opacityOverride ?? 1,
borderWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
strokeWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
lineWidth: options?.thickness ?? (opacityOverride ? 6 : 2),
...withCustomData(options),
};
};
const TOOL_DEFAULT_BUILDERS: Record<AnnotationToolId, ToolDefaultsBuilder> = {
select: () => null,
highlight: (options) => ({
type: PdfAnnotationSubtype.HIGHLIGHT,
color: options?.color ?? DEFAULTS.highlight,
opacity: options?.opacity ?? 0.6,
...withCustomData(options),
}),
underline: (options) => ({
type: PdfAnnotationSubtype.UNDERLINE,
color: options?.color ?? DEFAULTS.underline,
opacity: options?.opacity ?? 1,
...withCustomData(options),
}),
strikeout: (options) => ({
type: PdfAnnotationSubtype.STRIKEOUT,
color: options?.color ?? DEFAULTS.strikeout,
opacity: options?.opacity ?? 1,
...withCustomData(options),
}),
squiggly: (options) => ({
type: PdfAnnotationSubtype.SQUIGGLY,
color: options?.color ?? DEFAULTS.squiggly,
opacity: options?.opacity ?? 1,
...withCustomData(options),
}),
highlight: (options) => {
const colorValue = options?.color ?? DEFAULTS.highlight;
return {
type: PdfAnnotationSubtype.HIGHLIGHT,
strokeColor: colorValue,
color: colorValue,
opacity: options?.opacity ?? 0.6,
...withCustomData(options),
};
},
underline: (options) => {
const colorValue = options?.color ?? DEFAULTS.underline;
return {
type: PdfAnnotationSubtype.UNDERLINE,
strokeColor: colorValue,
color: colorValue,
opacity: options?.opacity ?? 1,
...withCustomData(options),
};
},
strikeout: (options) => {
const colorValue = options?.color ?? DEFAULTS.strikeout;
return {
type: PdfAnnotationSubtype.STRIKEOUT,
strokeColor: colorValue,
color: colorValue,
opacity: options?.opacity ?? 1,
...withCustomData(options),
};
},
squiggly: (options) => {
const colorValue = options?.color ?? DEFAULTS.squiggly;
return {
type: PdfAnnotationSubtype.SQUIGGLY,
strokeColor: colorValue,
color: colorValue,
opacity: options?.opacity ?? 1,
...withCustomData(options),
};
},
ink: (options) => buildInkDefaults(options),
inkHighlighter: (options) => buildInkDefaults(options, options?.opacity ?? 0.6),
text: (options) => ({
@@ -170,21 +194,21 @@ const TOOL_DEFAULT_BUILDERS: Record<AnnotationToolId, ToolDefaultsBuilder> = {
textAlign: options?.textAlign ?? 0,
opacity: options?.opacity ?? 1,
borderWidth: options?.thickness ?? 1,
...(options?.fillColor ? { backgroundColor: options.fillColor } : {}),
...(options?.fillColor ? { color: options.fillColor, interiorColor: options.fillColor } : {}),
...withCustomData(options),
}),
note: (options) => {
const backgroundColor = options?.fillColor ?? DEFAULTS.note;
const bgColor = options?.fillColor ?? DEFAULTS.note;
const fontColor = options?.color ?? DEFAULTS.text;
return {
type: PdfAnnotationSubtype.FREETEXT,
fontColor,
color: fontColor,
fontFamily: options?.fontFamily ?? 'Helvetica',
textAlign: options?.textAlign ?? 0,
fontSize: options?.fontSize ?? 12,
opacity: options?.opacity ?? 1,
backgroundColor,
color: bgColor,
interiorColor: bgColor,
borderWidth: options?.thickness ?? 0,
contents: options?.contents ?? 'Note',
icon: getIconEnum(options?.icon),
@@ -317,7 +341,11 @@ export const AnnotationAPIBridge = forwardRef<AnnotationAPI>(function Annotation
// `this`/state dependency (e.g. reading `selectedUid` from undefined).
// If that happens, fail gracefully and treat it as "no selection"
// instead of crashing the entire annotations tool.
console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error);
// Only log unexpected errors - "No active document" is a common expected state during init
const errorMessage = error instanceof Error ? error.message : String(error);
if (!errorMessage.includes('No active document')) {
console.error('[AnnotationAPIBridge] getSelectedAnnotation failed:', error);
}
return null;
}
},
@@ -1,6 +1,5 @@
import { useEffect, useMemo, useState, useCallback } from 'react';
import { useBookmarkCapability } from '@embedpdf/plugin-bookmark/react';
import { BookmarkCapability } from '@embedpdf/plugin-bookmark';
import { useBookmarkCapability, BookmarkCapability } from '@embedpdf/plugin-bookmark/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { BookmarkState, BookmarkAPIWrapper } from '@app/contexts/viewer/viewerBridges';
@@ -1,11 +1,12 @@
import { useState, useEffect, useMemo } from 'react';
import { useDocumentState } from '@embedpdf/core/react';
import { useSearch } from '@embedpdf/plugin-search/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { SEARCH_CONSTANTS } from '@app/components/viewer/constants/search';
interface SearchLayerProps {
documentId?: string;
pageIndex: number;
scale: number;
scale?: number;
highlightColor?: string;
activeHighlightColor?: string;
opacity?: number;
@@ -25,18 +26,22 @@ interface SearchResultState {
}
export function CustomSearchLayer({
documentId = '',
pageIndex,
scale,
scale: scaleProp,
highlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.BACKGROUND,
activeHighlightColor = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.ACTIVE_BACKGROUND,
opacity = SEARCH_CONSTANTS.HIGHLIGHT_COLORS.OPACITY,
padding = SEARCH_CONSTANTS.UI.HIGHLIGHT_PADDING,
borderRadius = 4
}: SearchLayerProps) {
const { provides: searchProvides } = useSearch();
const { scrollActions } = useViewer();
const { provides: searchProvides } = useSearch(documentId);
const documentState = useDocumentState(documentId);
const [searchResultState, setSearchResultState] = useState<SearchResultState | null>(null);
// Use document scale from EmbedPDF state, fallback to prop, then to 1
const scale = documentState?.scale ?? scaleProp ?? 1;
// Subscribe to search result state changes
useEffect(() => {
if (!searchProvides) {
@@ -45,16 +50,8 @@ export function CustomSearchLayer({
const unsubscribe = searchProvides.onSearchResultStateChange?.((state: SearchResultState) => {
if (!state) return;
// Auto-scroll to active search result
if (state.results && state.activeResultIndex !== undefined && state.activeResultIndex >= 0) {
const activeResult = state.results[state.activeResultIndex];
if (activeResult) {
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
scrollActions.scrollToPage(pageNumber);
}
}
// Only update state - do NOT auto-scroll here
// Scrolling should only happen when user explicitly navigates via next/previous
setSearchResultState(state);
});
@@ -0,0 +1,63 @@
import { useEffect, useMemo } from 'react';
import { useViewer } from '@app/contexts/ViewerContext';
import {
PdfPermissionFlag,
DocumentPermissionsState,
DocumentPermissionsAPIWrapper,
} from '@app/contexts/viewer/viewerBridges';
function hasPermissionFlag(permissions: number, flag: PdfPermissionFlag): boolean {
if (permissions === 0 || permissions === PdfPermissionFlag.AllowAll) {
return true;
}
return (permissions & flag) !== 0;
}
interface DocumentPermissionsAPIBridgeProps {
isEncrypted?: boolean;
isOwnerUnlocked?: boolean;
permissions?: number;
}
export function DocumentPermissionsAPIBridge({
isEncrypted = false,
isOwnerUnlocked = false,
permissions = PdfPermissionFlag.AllowAll,
}: DocumentPermissionsAPIBridgeProps) {
const { registerBridge } = useViewer();
const state = useMemo<DocumentPermissionsState>(() => ({
isEncrypted,
isOwnerUnlocked,
permissions,
canPrint: hasPermissionFlag(permissions, PdfPermissionFlag.Print),
canModifyContents: hasPermissionFlag(permissions, PdfPermissionFlag.ModifyContents),
canCopyContents: hasPermissionFlag(permissions, PdfPermissionFlag.CopyContents),
canModifyAnnotations: hasPermissionFlag(permissions, PdfPermissionFlag.ModifyAnnotations),
canFillForms: hasPermissionFlag(permissions, PdfPermissionFlag.FillForms),
canExtractForAccessibility: hasPermissionFlag(permissions, PdfPermissionFlag.ExtractForAccessibility),
canAssembleDocument: hasPermissionFlag(permissions, PdfPermissionFlag.AssembleDocument),
canPrintHighQuality: hasPermissionFlag(permissions, PdfPermissionFlag.PrintHighQuality),
}), [isEncrypted, isOwnerUnlocked, permissions]);
const api = useMemo<DocumentPermissionsAPIWrapper>(() => ({
hasPermission: (flag: PdfPermissionFlag) => hasPermissionFlag(permissions, flag),
hasAllPermissions: (flags: PdfPermissionFlag[]) =>
flags.every(flag => hasPermissionFlag(permissions, flag)),
getEffectivePermission: (flag: PdfPermissionFlag) => {
if (isOwnerUnlocked) return true;
return hasPermissionFlag(permissions, flag);
},
}), [permissions, isOwnerUnlocked]);
useEffect(() => {
registerBridge('permissions', {
state,
api,
});
}, [registerBridge, state, api]);
return null;
}
export { PdfPermissionFlag };
@@ -0,0 +1,56 @@
import React, { useState, useEffect } from 'react';
import { useDocumentManagerPlugin } from '@embedpdf/plugin-document-manager/react';
interface DocumentReadyWrapperProps {
children: (documentId: string) => React.ReactNode;
fallback?: React.ReactNode;
}
export function DocumentReadyWrapper({ children, fallback = null }: DocumentReadyWrapperProps) {
const { plugin, isLoading, ready } = useDocumentManagerPlugin();
const [activeDocumentId, setActiveDocumentId] = useState<string | null>(null);
useEffect(() => {
if (isLoading || !plugin) return;
const checkActiveDocument = async () => {
await ready;
// Try to get the active document from the plugin's provides()
const docManagerApi = plugin.provides?.();
if (docManagerApi) {
// Try different methods to get the active document
const activeDoc = docManagerApi.getActiveDocument?.();
if (activeDoc?.id) {
setActiveDocumentId(activeDoc.id);
return;
}
}
};
checkActiveDocument();
// Subscribe to document changes
const docManagerApi = plugin.provides?.();
if (docManagerApi?.onDocumentOpened) {
const unsubscribe = docManagerApi.onDocumentOpened((event: any) => {
const docId = event?.documentId || event?.id || event?.document?.id;
if (docId) {
setActiveDocumentId(docId);
}
});
return () => {
if (typeof unsubscribe === 'function') {
unsubscribe();
}
};
}
}, [plugin, isLoading, ready]);
if (!activeDocumentId) {
return <>{fallback}</>;
}
return <>{children(activeDocumentId)}</>;
}
@@ -345,21 +345,6 @@ const EmbedPdfViewerContent = ({
// Always consider applied redactions as unsaved until export
const hasAppliedRedactions = redactionsApplied;
// When in redact mode, still include annotation changes (users may draw)
if (isManualRedactMode) {
console.log('[Viewer] Checking for unsaved changes (redact mode):', {
hasPendingRedactions,
hasAppliedRedactions,
hasAnnotationChanges,
});
} else {
console.log('[Viewer] Checking for unsaved changes:', {
hasAnnotationChanges,
hasPendingRedactions,
hasAppliedRedactions,
});
}
return hasAnnotationChanges || hasPendingRedactions || hasAppliedRedactions;
};
+229 -157
View File
@@ -1,13 +1,17 @@
import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { useDocumentState } from '@embedpdf/core/react';
import { useAnnotationCapability } from '@embedpdf/plugin-annotation/react';
import { useScroll } from '@embedpdf/plugin-scroll/react';
import { PdfAnnotationSubtype } from '@embedpdf/models';
enum PDFActionType {
GoTo = 0,
GoToR = 1,
GoToE = 2,
URI = 3,
// Add other types as needed
Launch = 4,
Named = 5,
JavaScript = 6,
}
interface PDFRect {
@@ -46,24 +50,242 @@ function isURIAction(action: PDFAction): boolean {
return action.type === 'URI' || action.type === PDFActionType.URI;
}
function isInternalLink(link: LinkAnnotation): boolean {
// Utility functions for link type detection - prefixed to indicate future use
function _isInternalLink(link: LinkAnnotation): boolean {
return Boolean(link.target?.type === 'destination' ||
(link.target?.type === 'action' && link.target.action && isGoToAction(link.target.action)));
}
function isExternalLink(link: LinkAnnotation): boolean {
function _isExternalLink(link: LinkAnnotation): boolean {
return Boolean(link.target?.type === 'uri' ||
(link.target?.type === 'action' && link.target.action && isURIAction(link.target.action)));
}
interface LinkLayerProps {
documentId: string;
pageIndex: number;
scale: number;
document?: any;
pdfFile?: File | Blob;
onLinkClick?: (target: any) => void;
pageWidth: number;
pageHeight: number;
}
export const LinkLayer: React.FC<LinkLayerProps> = ({
documentId,
pageIndex,
pageWidth,
pageHeight,
}) => {
const { provides: annotation } = useAnnotationCapability();
const { provides: scroll } = useScroll(documentId);
const documentState = useDocumentState(documentId);
const [links, setLinks] = useState<LinkAnnotation[]>([]);
const [isNavigating, setIsNavigating] = useState(false);
// Get original PDF page dimensions from document state
const pdfPage = documentState?.document?.pages?.[pageIndex];
const pdfPageWidth = pdfPage?.size?.width ?? 0;
const pdfPageHeight = pdfPage?.size?.height ?? 0;
// Process links with proper coordinate transformation
const processedLinks = useMemo(() => {
if (!pageWidth || !pageHeight || !pdfPageWidth || !pdfPageHeight) return [];
// Use the document scale like EmbedPDF's AnnotationLayer does
const scale = documentState?.scale ?? 1;
return links.map(link => {
const { origin, size } = link.rect;
// Use document scale like EmbedPDF does
const scaledLeft = origin.x * scale;
const scaledTop = origin.y * scale;
const scaledWidth = size.width * scale;
const scaledHeight = size.height * scale;
return {
...link,
scaledRect: {
left: scaledLeft,
top: scaledTop,
width: scaledWidth,
height: scaledHeight,
},
};
});
}, [links, pageWidth, pageHeight, pdfPageWidth, pdfPageHeight, documentState?.scale]);
useEffect(() => {
const fetchLinks = async () => {
if (!annotation) return;
try {
const pageAnnotationsResult = annotation.getPageAnnotations({
pageIndex,
});
const resolveAnnotations = async (result: unknown): Promise<any[]> => {
if (result && typeof (result as any).toPromise === 'function') {
return (result as any).toPromise();
}
if (result && typeof (result as any).then === 'function') {
return result as Promise<any[]>;
}
if (Array.isArray(result)) {
return result;
}
return [];
};
const pageAnnotations = await resolveAnnotations(pageAnnotationsResult);
const linkAnnotations = pageAnnotations.filter(
(ann: any) => ann.type === 2 || ann.type === PdfAnnotationSubtype.LINK
) as LinkAnnotation[];
if (linkAnnotations.length > 0) {
setLinks(linkAnnotations);
}
} catch (error) {
console.error('[LinkLayer] Failed to fetch links from annotation API:', {
error,
pageIndex,
});
}
};
fetchLinks();
}, [annotation, pageIndex]);
const handleLinkClick = useCallback(async (link: LinkAnnotation) => {
if (isNavigating) return;
try {
setIsNavigating(true);
// Try to extract destination from various possible locations in the link object
const linkData = link as any;
// Check for destination in various locations
let targetPage: number | undefined;
let uri: string | undefined;
// Try target.destination first
if (link.target?.destination?.pageIndex !== undefined) {
targetPage = link.target.destination.pageIndex;
}
// Try target.action.destination
else if (link.target?.action?.destination?.pageIndex !== undefined) {
targetPage = link.target.action.destination.pageIndex;
}
// Try direct dest property (PDF.js style)
else if (linkData.dest?.pageIndex !== undefined) {
targetPage = linkData.dest.pageIndex;
}
// Try destination at root level
else if (linkData.destination?.pageIndex !== undefined) {
targetPage = linkData.destination.pageIndex;
}
// Try action at root level
else if (linkData.action?.destination?.pageIndex !== undefined) {
targetPage = linkData.action.destination.pageIndex;
}
// Check for URI in various locations
if (link.target?.uri) {
uri = link.target.uri;
} else if (link.target?.action?.uri) {
uri = link.target.action.uri;
} else if (linkData.uri) {
uri = linkData.uri;
} else if (linkData.url) {
uri = linkData.url;
} else if (linkData.action?.uri) {
uri = linkData.action.uri;
}
if (targetPage !== undefined && scroll) {
scroll.scrollToPage({
pageNumber: targetPage + 1,
behavior: 'smooth',
});
} else if (uri) {
try {
const url = new URL(uri, window.location.href);
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
window.open(uri, '_blank', 'noopener,noreferrer');
} else {
console.warn('[LinkLayer] Blocked potentially unsafe URL protocol:', url.protocol);
}
} catch {
window.open(uri, '_blank', 'noopener,noreferrer');
}
} else {
console.warn('[LinkLayer] Could not extract destination or URI from link:', link);
}
} catch (error) {
console.error('[LinkLayer] Navigation failed:', error);
} finally {
setIsNavigating(false);
}
}, [isNavigating, scroll]);
return (
<div
className="absolute inset-0"
style={{
pointerEvents: 'none',
zIndex: 10, // Above selection layer but below UI controls
}}
>
{processedLinks.map((link) => {
const { id } = link;
const { left, top, width, height } = link.scaledRect;
return (
<a
key={id}
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleLinkClick(link);
}}
onMouseDown={(e) => e.stopPropagation()}
className={`absolute block cursor-pointer ${isNavigating ? 'cursor-not-allowed' : ''}`}
style={{
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
minWidth: '8px',
minHeight: '8px',
pointerEvents: 'auto',
backgroundColor: 'transparent',
zIndex: 11,
border: '2px solid transparent',
borderRadius: '2px',
transition: 'background-color 0.15s ease-in-out, border-color 0.15s ease-in-out',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(59, 130, 246, 0.15)';
e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.borderColor = 'transparent';
}}
title={getLinkTitle(link)}
aria-label={getLinkAriaLabel(link)}
>
{/* Invisible clickable area */}
</a>
);
})}
</div>
);
};
const getLinkTitle = (link: LinkAnnotation): string => {
if (link.target?.type === 'destination') {
return `Go to page ${(link.target.destination?.pageIndex ?? 0) + 1}`;
@@ -92,153 +314,3 @@ const getLinkAriaLabel = (link: LinkAnnotation): string => {
}
return 'Open external link';
};
export const LinkLayer: React.FC<LinkLayerProps> = ({
pageIndex,
scale,
document: pdfDocument,
onLinkClick
}) => {
const { provides: annotation } = useAnnotationCapability();
const { provides: scroll } = useScroll();
const [links, setLinks] = useState<LinkAnnotation[]>([]);
const [isNavigating, setIsNavigating] = useState(false);
const processedLinks = useMemo(() => {
return links.map(link => ({
...link,
scaledRect: {
left: link.rect.origin.x * scale,
top: link.rect.origin.y * scale,
width: link.rect.size.width * scale,
height: link.rect.size.height * scale,
}
}));
}, [links, scale]);
useEffect(() => {
const fetchLinks = async () => {
if (!annotation) return;
try {
// Use the annotation API's built-in filtering if available
const pageAnnotations = await annotation
.getPageAnnotations({
pageIndex,
// Try to filter for link annotations (type 2) if the API supports it
...(annotation.getPageAnnotations.length > 1 ? { types: [2] } : {})
})
.toPromise();
// Filter for link annotations (type 2 is LINK in PDF spec) as fallback
const linkAnnotations = pageAnnotations.filter(
(ann: any) => ann.type === 2
) as LinkAnnotation[];
if (linkAnnotations.length > 0) {
setLinks(linkAnnotations);
return;
}
} catch (error) {
console.error('[LinkLayer] Failed to fetch links from annotation API:', error);
}
if (pdfDocument) {
try {
// Try different methods to get link annotations
let pdfLinks: any[] = [];
if (pdfDocument.getPageAnnotations && typeof pdfDocument.getPageAnnotations === 'function') {
pdfLinks = await pdfDocument.getPageAnnotations(pageIndex);
} else if (pdfDocument.getAnnotations && typeof pdfDocument.getAnnotations === 'function') {
const allAnnotations = await pdfDocument.getAnnotations();
pdfLinks = allAnnotations.filter((ann: any) => ann.pageIndex === pageIndex && ann.type === 2);
} else if (pdfDocument.pages && pdfDocument.pages[pageIndex]) {
const page = pdfDocument.pages[pageIndex];
if (page.getAnnotations && typeof page.getAnnotations === 'function') {
pdfLinks = await page.getAnnotations();
}
}
const convertedLinks = pdfLinks.map((ann: any) => ({
id: ann.id || `pdf-link-${pageIndex}-${Math.random()}`,
type: ann.type || 2,
rect: ann.rect || ann,
target: ann.target || ann.action
})) as LinkAnnotation[];
setLinks(convertedLinks);
} catch (error) {
console.warn('[LinkLayer] Failed to get annotations from PDF document:', error);
}
} else {
console.warn('[LinkLayer] No annotation API or PDF document available');
}
};
fetchLinks();
}, [annotation, pageIndex, pdfDocument]);
const handleLinkClick = useCallback(async (link: LinkAnnotation) => {
if (isNavigating) return; // Prevent multiple simultaneous navigations
try {
setIsNavigating(true);
if (onLinkClick) {
onLinkClick(link.target);
return;
}
if (isInternalLink(link)) {
const targetPage = link.target?.destination?.pageIndex ??
link.target?.action?.destination?.pageIndex;
if (targetPage !== undefined && scroll) {
await scroll.scrollToPage({
pageNumber: targetPage + 1, // PDF pages are 1-indexed
behavior: 'smooth',
});
}
} else if (isExternalLink(link)) {
const uri = link.target?.uri ?? link.target?.action?.uri;
if (uri) {
window.open(uri, '_blank', 'noopener,noreferrer');
}
} else {
throw new Error(`Unsupported link type: ${link.target?.type}`);
}
} catch (error) {
console.error('[LinkLayer] Navigation failed:', error);
} finally {
setIsNavigating(false);
}
}, [isNavigating, onLinkClick, scroll]);
return (
<div className="absolute inset-0 pointer-events-none">
{processedLinks.map((link) => {
const { id } = link;
const { left, top, width, height } = link.scaledRect;
return (
<button
key={id}
onClick={() => handleLinkClick(link)}
disabled={isNavigating}
className={`absolute opacity-0 hover:opacity-20 bg-blue-500 transition-opacity cursor-pointer pointer-events-auto border border-blue-400 hover:border-blue-600 ${isNavigating ? 'cursor-not-allowed opacity-50' : ''}`}
style={{
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height}px`,
minWidth: '8px',
minHeight: '8px',
}}
title={getLinkTitle(link)}
aria-label={getLinkAriaLabel(link)}
/>
);
})}
</div>
);
};
@@ -6,8 +6,8 @@ import { PrivateContent } from '@app/components/shared/PrivateContent';
// 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 { Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react';
import { DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/react';
import { RenderPluginPackage } from '@embedpdf/plugin-render/react';
import { ZoomPluginPackage, ZoomMode } from '@embedpdf/plugin-zoom/react';
import { InteractionManagerPluginPackage, PagePointerProvider, GlobalPointerProvider } from '@embedpdf/plugin-interaction-manager/react';
@@ -19,7 +19,7 @@ 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 { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark';
import { BookmarkPluginPackage } from '@embedpdf/plugin-bookmark/react';
import { PrintPluginPackage } from '@embedpdf/plugin-print/react';
import { HistoryPluginPackage } from '@embedpdf/plugin-history/react';
import { AnnotationLayer, AnnotationPluginPackage } from '@embedpdf/plugin-annotation/react';
@@ -49,8 +49,13 @@ import { LinkLayer } from '@app/components/viewer/LinkLayer';
import { RedactionSelectionMenu } from '@app/components/viewer/RedactionSelectionMenu';
import { RedactionPendingTracker, RedactionPendingTrackerAPI } from '@app/components/viewer/RedactionPendingTracker';
import { RedactionAPIBridge } from '@app/components/viewer/RedactionAPIBridge';
import { DocumentPermissionsAPIBridge } from '@app/components/viewer/DocumentPermissionsAPIBridge';
import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrapper';
import { ActiveDocumentProvider } from '@app/components/viewer/ActiveDocumentContext';
import { absoluteWithBasePath } from '@app/constants/app';
const DOCUMENT_NAME = 'stirling-pdf-viewer';
interface LocalEmbedPDFProps {
file?: File | Blob;
url?: string | null;
@@ -90,22 +95,16 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
const viewportGap = rootFontSize * 3.5;
return [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
type: 'url',
pdfFile: {
id: 'stirling-pdf-viewer',
url: pdfUrl,
},
},
createPluginRegistration(DocumentManagerPluginPackage, {
initialDocuments: [{
url: pdfUrl,
name: DOCUMENT_NAME,
}],
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap,
}),
createPluginRegistration(ScrollPluginPackage, {
strategy: ScrollStrategy.Vertical,
initialPage: 0,
}),
createPluginRegistration(ScrollPluginPackage),
createPluginRegistration(RenderPluginPackage, {
withForms: true,
withAnnotations: showBakedAnnotations && !enableAnnotations, // Show baked annotations only when: visibility is ON and annotation layer is OFF
@@ -257,7 +256,8 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
<EmbedPDF
engine={engine}
plugins={plugins}
onInitialized={async (registry) => {
onInitialized={async (registry: any) => {
// v2.0: Use registry.getPlugin() to access plugin APIs
const annotationPlugin = registry.getPlugin('annotation');
if (!annotationPlugin || !annotationPlugin.provides) return;
@@ -279,6 +279,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.HIGHLIGHT ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.HIGHLIGHT,
strokeColor: '#ffd54f',
color: '#ffd54f',
opacity: 0.6,
},
@@ -295,6 +296,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.UNDERLINE ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.UNDERLINE,
strokeColor: '#ffb300',
color: '#ffb300',
opacity: 1,
},
@@ -311,6 +313,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.STRIKEOUT ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.STRIKEOUT,
strokeColor: '#e53935',
color: '#e53935',
opacity: 1,
},
@@ -327,6 +330,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.SQUIGGLY ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.SQUIGGLY,
strokeColor: '#00acc1',
color: '#00acc1',
opacity: 1,
},
@@ -343,6 +347,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK ? 10 : 0),
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#1f2933',
color: '#1f2933',
opacity: 1,
borderWidth: 2,
@@ -359,9 +364,10 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
id: 'inkHighlighter',
name: 'Ink Highlighter',
interaction: { exclusive: true, cursor: 'crosshair' },
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && annotation.color === '#ffd54f' ? 8 : 0),
matchScore: (annotation: any) => (annotation.type === PdfAnnotationSubtype.INK && (annotation.strokeColor === '#ffd54f' || annotation.color === '#ffd54f') ? 8 : 0),
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#ffd54f',
color: '#ffd54f',
opacity: 0.5,
borderWidth: 6,
@@ -590,6 +596,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
matchScore: () => 0,
defaults: {
type: PdfAnnotationSubtype.INK,
strokeColor: '#000000',
color: '#000000',
opacity: 1.0,
borderWidth: 2,
@@ -622,6 +629,7 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
}
}}
>
<ActiveDocumentProvider>
<ZoomAPIBridge />
<ScrollAPIBridge />
<SelectionAPIBridge />
@@ -641,87 +649,98 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, enableReda
<ExportAPIBridge />
<BookmarkAPIBridge />
<PrintAPIBridge />
<GlobalPointerProvider>
<Viewport
style={{
backgroundColor: 'var(--bg-background)',
height: '100%',
width: '100%',
maxHeight: '100%',
maxWidth: '100%',
overflow: 'auto',
position: 'relative',
flex: 1,
minHeight: 0,
minWidth: 0,
contain: 'strict',
}}
>
<Scroller
renderPage={({ document, width, height, pageIndex, scale, rotation }) => {
return (
<Rotate key={document?.id} pageSize={{ width, height }}>
<PagePointerProvider pageIndex={pageIndex} pageWidth={width} pageHeight={height} scale={scale} rotation={rotation}>
<div
data-page-index={pageIndex}
data-page-width={width}
data-page-height={height}
style={{
width,
height,
position: 'relative',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
{/* High-resolution tile layer */}
<TilingLayer pageIndex={pageIndex} scale={scale} />
<DocumentPermissionsAPIBridge />
<DocumentReadyWrapper
fallback={
<Center style={{ height: '100%', width: '100%' }}>
<ToolLoadingFallback />
</Center>
}
>
{(documentId) => (
<GlobalPointerProvider documentId={documentId}>
<Viewport
documentId={documentId}
style={{
backgroundColor: 'var(--bg-background)',
height: '100%',
width: '100%',
maxHeight: '100%',
maxWidth: '100%',
overflow: 'auto',
position: 'relative',
flex: 1,
minHeight: 0,
minWidth: 0,
contain: 'strict',
}}
>
<Scroller
documentId={documentId}
renderPage={({ width, height, pageIndex }) => {
return (
<Rotate key={`${documentId}-${pageIndex}`} documentId={documentId} pageIndex={pageIndex}>
<PagePointerProvider documentId={documentId} pageIndex={pageIndex}>
<div
data-page-index={pageIndex}
data-page-width={width}
data-page-height={height}
style={{
width,
height,
position: 'relative',
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)'
}}
draggable={false}
onDragStart={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
onDragOver={(e) => e.preventDefault()}
>
<TilingLayer documentId={documentId} pageIndex={pageIndex} />
{/* Search highlight layer */}
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
<CustomSearchLayer documentId={documentId} pageIndex={pageIndex} />
{/* Selection layer for text interaction */}
<SelectionLayer pageIndex={pageIndex} scale={scale} />
<SelectionLayer documentId={documentId} pageIndex={pageIndex} />
{/* Link layer for clickable PDF links */}
<LinkLayer pageIndex={pageIndex} scale={scale} document={document} pdfFile={file} />
{/* AnnotationLayer for annotation editing (only when enabled) */}
{enableAnnotations && (
<AnnotationLayer
documentId={documentId}
pageIndex={pageIndex}
selectionOutlineColor="#007ACC"
/>
)}
{/* Annotation layer for signatures (only when enabled) */}
{enableAnnotations && (
<AnnotationLayer
pageIndex={pageIndex}
scale={scale}
pageWidth={width}
pageHeight={height}
rotation={rotation}
selectionOutlineColor="#007ACC"
/>
)}
{enableRedaction && (
<RedactionLayer
documentId={documentId}
pageIndex={pageIndex}
selectionMenu={(props) => <RedactionSelectionMenu {...props} />}
/>
)}
{/* Redaction layer for marking areas to redact (only when enabled) */}
{enableRedaction && (
<RedactionLayer
pageIndex={pageIndex}
scale={scale}
rotation={rotation}
selectionMenu={(props) => <RedactionSelectionMenu {...props} />}
/>
)}
</div>
</PagePointerProvider>
</Rotate>
);
}}
/>
</Viewport>
</GlobalPointerProvider>
{/* LinkLayer on top to handle link navigation - must be last for click priority */}
<LinkLayer
documentId={documentId}
pageIndex={pageIndex}
pageWidth={width}
pageHeight={height}
/>
</div>
</PagePointerProvider>
</Rotate>
);
}}
/>
</Viewport>
</GlobalPointerProvider>
)}
</DocumentReadyWrapper>
</ActiveDocumentProvider>
</EmbedPDF>
</div>
</PrivateContent>
@@ -5,8 +5,8 @@ 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 { Scroller, ScrollPluginPackage } from '@embedpdf/plugin-scroll/react';
import { DocumentManagerPluginPackage } from '@embedpdf/plugin-document-manager/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';
@@ -17,12 +17,11 @@ 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 { Rotation, PdfAnnotationSubtype } 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 '@app/components/viewer/CustomSearchLayer';
import { ZoomAPIBridge } from '@app/components/viewer/ZoomAPIBridge';
@@ -35,6 +34,9 @@ import { SpreadAPIBridge } from '@app/components/viewer/SpreadAPIBridge';
import { SearchAPIBridge } from '@app/components/viewer/SearchAPIBridge';
import { ThumbnailAPIBridge } from '@app/components/viewer/ThumbnailAPIBridge';
import { RotateAPIBridge } from '@app/components/viewer/RotateAPIBridge';
import { DocumentReadyWrapper } from '@app/components/viewer/DocumentReadyWrapper';
const DOCUMENT_NAME = 'stirling-pdf-signing-viewer';
interface LocalEmbedPDFWithAnnotationsProps {
file?: File | Blob;
@@ -69,22 +71,16 @@ export function LocalEmbedPDFWithAnnotations({
const viewportGap = rootFontSize * 3.5;
return [
createPluginRegistration(LoaderPluginPackage, {
loadingOptions: {
type: 'url',
pdfFile: {
id: 'stirling-pdf-signing-viewer',
url: pdfUrl,
},
},
createPluginRegistration(DocumentManagerPluginPackage, {
initialDocuments: [{
url: pdfUrl,
name: DOCUMENT_NAME,
}],
}),
createPluginRegistration(ViewportPluginPackage, {
viewportGap,
}),
createPluginRegistration(ScrollPluginPackage, {
strategy: ScrollStrategy.Vertical,
initialPage: 0,
}),
createPluginRegistration(ScrollPluginPackage),
createPluginRegistration(RenderPluginPackage, {
withForms: true,
withAnnotations: true,
@@ -191,7 +187,8 @@ export function LocalEmbedPDFWithAnnotations({
<EmbedPDF
engine={engine}
plugins={plugins}
onInitialized={async (registry) => {
onInitialized={async (registry: any) => {
// v2.0: Use registry.getPlugin() to access plugin APIs
const annotationPlugin = registry.getPlugin('annotation');
if (!annotationPlugin || !annotationPlugin.provides) return;
@@ -244,78 +241,71 @@ export function LocalEmbedPDFWithAnnotations({
<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} />
<DocumentReadyWrapper
fallback={
<Center style={{ height: '100%', width: '100%' }}>
<ToolLoadingFallback />
</Center>
}
>
{(documentId) => (
<GlobalPointerProvider documentId={documentId}>
<Viewport
documentId={documentId}
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
documentId={documentId}
renderPage={({ width, height, pageIndex }) => (
<Rotate key={`${documentId}-${pageIndex}`} documentId={documentId} pageIndex={pageIndex}>
<PagePointerProvider documentId={documentId} pageIndex={pageIndex}>
<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()}
>
<TilingLayer documentId={documentId} pageIndex={pageIndex} />
{/* Search highlight layer */}
<CustomSearchLayer pageIndex={pageIndex} scale={scale} />
<CustomSearchLayer documentId={documentId} pageIndex={pageIndex} />
{/* Selection layer for text interaction */}
<SelectionLayer pageIndex={pageIndex} scale={scale} />
<SelectionLayer documentId={documentId} pageIndex={pageIndex} />
{/* Annotation layer for signatures */}
<AnnotationLayer
pageIndex={pageIndex}
scale={scale}
pageWidth={width}
pageHeight={height}
rotation={rotation || 0}
selectionOutlineColor="#007ACC"
/>
</div>
</PagePointerProvider>
</Rotate>
)}
/>
</Viewport>
</GlobalPointerProvider>
<AnnotationLayer
documentId={documentId}
pageIndex={pageIndex}
selectionOutlineColor="#007ACC"
/>
</div>
</PagePointerProvider>
</Rotate>
)}
/>
</Viewport>
</GlobalPointerProvider>
)}
</DocumentReadyWrapper>
</EmbedPDF>
</div>
);
@@ -1,44 +1,60 @@
import { useEffect, useState, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { usePan } from '@embedpdf/plugin-pan/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* Component that runs inside EmbedPDF context and updates pan state in ViewerContext
*/
export function PanAPIBridge() {
const { provides: pan, isPanning } = usePan();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <PanAPIBridgeInner documentId={activeDocumentId} />;
}
function PanAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: pan, isPanning } = usePan(documentId);
const { registerBridge, triggerImmediatePanUpdate } = useViewer();
// Store state locally
const [_localState, setLocalState] = useState({
isPanning: false
});
// Keep pan ref updated to avoid re-running effect when object reference changes
const panRef = useRef(pan);
useEffect(() => {
panRef.current = pan;
}, [pan]);
// Track previous isPanning value to detect changes
const prevIsPanningRef = useRef<boolean>(isPanning);
useEffect(() => {
if (pan) {
// Update local state
const currentPan = panRef.current;
if (currentPan) {
const newState = {
isPanning
};
setLocalState(newState);
// Register this bridge with ViewerContext
registerBridge('pan', {
state: newState,
api: {
enable: () => {
pan.enablePan();
currentPan.enablePan();
},
disable: () => {
pan.disablePan();
currentPan.disablePan();
},
toggle: () => {
pan.togglePan();
currentPan.togglePan();
},
makePanDefault: () => {
// v2.3.0: makePanDefault may not exist, enable pan as fallback
if ('makePanDefault' in currentPan && typeof (currentPan as any).makePanDefault === 'function') {
(currentPan as any).makePanDefault();
} else {
currentPan.enablePan();
}
},
makePanDefault: () => pan.makePanDefault(),
}
});
@@ -48,7 +64,7 @@ export function PanAPIBridge() {
triggerImmediatePanUpdate(isPanning);
}
}
}, [pan, isPanning, triggerImmediatePanUpdate]);
}, [isPanning, registerBridge, triggerImmediatePanUpdate]);
return null;
}
@@ -1,17 +1,21 @@
import { useEffect, useImperativeHandle } from 'react';
import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react';
import { useRedaction } from '@app/contexts/RedactionContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* RedactionAPIBridge connects the EmbedPDF redaction plugin to our RedactionContext.
* It must be rendered inside the EmbedPDF context to access the plugin API.
*
* It does two things:
* 1. Syncs EmbedPDF state (pendingCount, activeType, isRedacting) to our context
* 2. Exposes the EmbedPDF API through our context's ref so outside components can call it
*/
export function RedactionAPIBridge() {
const { state, provides } = useEmbedPdfRedaction();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <RedactionAPIBridgeInner documentId={activeDocumentId} />;
}
function RedactionAPIBridgeInner({ documentId }: { documentId: string }) {
const { state, provides } = useEmbedPdfRedaction(documentId);
const {
redactionApiRef,
setPendingCount,
@@ -1,22 +1,28 @@
import { useEffect, useRef, useImperativeHandle, forwardRef } from 'react';
import { useRedaction as useEmbedPdfRedaction } from '@embedpdf/plugin-redaction/react';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
export interface RedactionPendingTrackerAPI {
commitAllPending: () => void;
getPendingCount: () => number;
}
/**
* RedactionPendingTracker monitors pending redactions and exposes an API
* for committing and checking pending redactions.
* Must be rendered inside the EmbedPDF context.
*
* Note: The unsaved changes checker is registered by EmbedPdfViewer, not here,
* to avoid conflicts and allow the viewer to check both annotations and redactions.
*/
export const RedactionPendingTracker = forwardRef<RedactionPendingTrackerAPI>(
function RedactionPendingTracker(_, ref) {
const { state, provides } = useEmbedPdfRedaction();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <RedactionPendingTrackerInner documentId={activeDocumentId} ref={ref} />;
}
);
const RedactionPendingTrackerInner = forwardRef<RedactionPendingTrackerAPI, { documentId: string }>(
function RedactionPendingTrackerInner({ documentId }, ref) {
const { state, provides } = useEmbedPdfRedaction(documentId);
const pendingCountRef = useRef(0);
@@ -1,4 +1,4 @@
import { useRedaction as useEmbedPdfRedaction, SelectionMenuProps } from '@embedpdf/plugin-redaction/react';
import { useRedaction as useEmbedPdfRedaction, RedactionSelectionMenuProps } from '@embedpdf/plugin-redaction/react';
import { ActionIcon, Tooltip, Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { createPortal } from 'react-dom';
@@ -6,46 +6,63 @@ import { useEffect, useState, useRef, useCallback } from 'react';
import DeleteIcon from '@mui/icons-material/Delete';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { useRedaction } from '@app/contexts/RedactionContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* Custom menu component that appears when a pending redaction mark is selected.
* Allows users to remove or apply individual pending marks.
* Uses a portal to ensure it appears above all content, including next pages.
*/
export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: SelectionMenuProps) {
const { t } = useTranslation();
const { provides } = useEmbedPdfRedaction();
const { setRedactionsApplied } = useRedaction();
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
const wrapperRef = useRef<HTMLDivElement>(null);
// Use the official EmbedPDF v2.3.0 types
export type { RedactionSelectionMenuProps };
export function RedactionSelectionMenu(props: RedactionSelectionMenuProps) {
const activeDocumentId = useActiveDocumentId();
// Merge refs if menuWrapperProps has a ref
// Don't render until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return (
<RedactionSelectionMenuInner
documentId={activeDocumentId}
{...props}
/>
);
}
function RedactionSelectionMenuInner({
documentId,
context,
selected,
menuWrapperProps,
}: RedactionSelectionMenuProps & { documentId: string }) {
// Extract item and pageIndex from context (EmbedPDF v2.3.0 API)
const item = context?.item;
const pageIndex = context?.pageIndex;
const { t } = useTranslation();
const { provides } = useEmbedPdfRedaction(documentId);
const { setRedactionsApplied } = useRedaction();
const wrapperRef = useRef<HTMLDivElement>(null);
const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null);
// Merge refs - menuWrapperProps.ref is a callback ref
const setRef = useCallback((node: HTMLDivElement | null) => {
wrapperRef.current = node;
if (menuWrapperProps?.ref) {
const ref = menuWrapperProps.ref;
if (typeof ref === 'function') {
ref(node);
} else if (ref && 'current' in ref) {
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
}
}
// Call the EmbedPDF ref callback
menuWrapperProps?.ref?.(node);
}, [menuWrapperProps]);
const handleRemove = useCallback(() => {
if (provides?.removePending && item) {
provides.removePending(item.page, item.id);
if (provides?.removePending && item && pageIndex !== undefined) {
provides.removePending(pageIndex, item.id);
}
}, [provides, item]);
}, [provides, item, pageIndex]);
const handleApply = useCallback(() => {
if (provides?.commitPending && item) {
provides.commitPending(item.page, item.id);
if (provides?.commitPending && item && pageIndex !== undefined) {
provides.commitPending(pageIndex, item.id);
// Mark redactions as applied (but not yet saved) so the Save Changes button stays enabled
// This ensures the button doesn't become disabled when pendingCount decreases
setRedactionsApplied(true);
}
}, [provides, item, setRedactionsApplied]);
}, [provides, item, pageIndex, setRedactionsApplied]);
// Calculate position for portal based on wrapper element
useEffect(() => {
@@ -61,13 +78,13 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel
return;
}
const rect = wrapper.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
// Position menu below the wrapper, centered
// Use getBoundingClientRect which gives viewport-relative coordinates
// Since we're using fixed positioning in the portal, we don't need to add scroll offsets
setMenuPosition({
top: rect.bottom + 8,
left: rect.left + rect.width / 2,
top: wrapperRect.bottom + 8,
left: wrapperRect.left + wrapperRect.width / 2,
});
};
@@ -152,19 +169,16 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel
</div>
) : null;
// Extract ref from menuWrapperProps to avoid conflicts
const { ref: _, ...wrapperPropsWithoutRef } = menuWrapperProps || {};
return (
<>
{/* Invisible wrapper that provides positioning - uses EmbedPDF's menuWrapperProps */}
<div
ref={setRef}
{...wrapperPropsWithoutRef}
style={{
// Preserve the original positioning from menuWrapperProps
...(wrapperPropsWithoutRef?.style || {}),
// Override visibility to hide the wrapper (we only need its position)
visibility: 'hidden',
// Use EmbedPDF's positioning styles
...menuWrapperProps?.style,
// Keep the wrapper invisible but still occupying space for positioning
opacity: 0,
pointerEvents: 'none',
}}
/>
@@ -174,4 +188,3 @@ export function RedactionSelectionMenu({ item, selected, menuWrapperProps }: Sel
</>
);
}
@@ -1,39 +1,48 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef } from 'react';
import { useRotate } from '@embedpdf/plugin-rotate/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* Component that runs inside EmbedPDF context and updates rotation state in ViewerContext
*/
export function RotateAPIBridge() {
const { provides: rotate, rotation } = useRotate();
const { registerBridge } = useViewer();
const activeDocumentId = useActiveDocumentId();
// Store state locally
const [_localState, setLocalState] = useState({
rotation: 0
});
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <RotateAPIBridgeInner documentId={activeDocumentId} />;
}
function RotateAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: rotate, rotation } = useRotate(documentId);
const { registerBridge } = useViewer();
// Keep rotate ref updated to avoid re-running effect when object reference changes
const rotateRef = useRef(rotate);
useEffect(() => {
rotateRef.current = rotate;
}, [rotate]);
useEffect(() => {
if (rotate) {
// Update local state
const currentRotate = rotateRef.current;
if (currentRotate) {
const newState = {
rotation
};
setLocalState(newState);
// Register this bridge with ViewerContext
registerBridge('rotation', {
state: newState,
api: {
rotateForward: () => rotate.rotateForward(),
rotateBackward: () => rotate.rotateBackward(),
setRotation: (rotationValue: number) => rotate.setRotation(rotationValue),
rotateForward: () => currentRotate.rotateForward(),
rotateBackward: () => currentRotate.rotateBackward(),
setRotation: (rotationValue: number) => currentRotate.setRotation(rotationValue),
getRotation: () => rotation,
}
});
}
}, [rotate, rotation]);
}, [rotation, registerBridge]);
return null;
}
@@ -1,20 +1,39 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useScroll } from '@embedpdf/plugin-scroll/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* ScrollAPIBridge manages scroll state and exposes scroll actions.
* Registers with ViewerContext to provide scroll functionality to UI components.
*/
export function ScrollAPIBridge() {
const { provides: scroll, state: scrollState } = useScroll();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <ScrollAPIBridgeInner documentId={activeDocumentId} />;
}
function ScrollAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: scroll, state: scrollState } = useScroll(documentId);
const { registerBridge, triggerImmediateScrollUpdate } = useViewer();
// Keep scroll ref updated to avoid re-running effect when object reference changes
const scrollRef = useRef(scroll);
useEffect(() => {
if (scroll && scrollState) {
scrollRef.current = scroll;
}, [scroll]);
// Extract primitive values to avoid dependency on object references
const currentPage = scrollState?.currentPage;
const totalPages = scrollState?.totalPages;
useEffect(() => {
const currentScroll = scrollRef.current;
if (currentScroll && currentPage !== undefined && totalPages !== undefined) {
const newState = {
currentPage: scrollState.currentPage,
totalPages: scrollState.totalPages,
currentPage,
totalPages,
};
// Trigger immediate update for responsive UI
@@ -22,10 +41,10 @@ export function ScrollAPIBridge() {
registerBridge('scroll', {
state: newState,
api: scroll
api: currentScroll
});
}
}, [scroll, scrollState]);
}, [currentPage, totalPages, registerBridge, triggerImmediateScrollUpdate]);
return null;
}
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { useEffect, useState, useRef } from 'react';
import { useSearch } from '@embedpdf/plugin-search/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
interface SearchResult {
pageIndex: number;
@@ -10,13 +11,29 @@ interface SearchResult {
}>;
}
/**
* SearchAPIBridge manages search state and provides search functionality.
* Listens for search result changes from EmbedPDF and maintains local state.
*/
export function SearchAPIBridge() {
const { provides: search } = useSearch();
const { registerBridge } = useViewer();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <SearchAPIBridgeInner documentId={activeDocumentId} />;
}
function SearchAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: search } = useSearch(documentId);
const { registerBridge, scrollActions } = useViewer();
// Keep search ref updated to avoid re-running effects when object reference changes
const searchRef = useRef(search);
const isSearchingRef = useRef(false);
const lastScrolledIndexRef = useRef<number | null>(null);
useEffect(() => {
searchRef.current = search;
}, [search]);
const [localState, setLocalState] = useState({
results: null as SearchResult[] | null,
@@ -24,10 +41,18 @@ export function SearchAPIBridge() {
});
// Subscribe to search result changes from EmbedPDF
const subscriptionRef = useRef<(() => void) | null>(null);
useEffect(() => {
// Cleanup previous subscription
if (subscriptionRef.current) {
subscriptionRef.current();
subscriptionRef.current = null;
}
if (!search) return;
const unsubscribe = search.onSearchResultStateChange?.((state: any) => {
subscriptionRef.current = search.onSearchResultStateChange?.((state: any) => {
if (!state) return;
const newState = {
@@ -42,27 +67,80 @@ export function SearchAPIBridge() {
}
return prevState;
});
});
}) ?? null;
return unsubscribe;
return () => {
if (subscriptionRef.current) {
subscriptionRef.current();
subscriptionRef.current = null;
}
};
}, [search]);
// Register bridge whenever search API or state changes
// Extract primitive values from localState to avoid object reference dependencies
const localResults = localState.results;
const localActiveIndex = localState.activeIndex;
// Scroll to active result when it changes (for next/previous navigation)
useEffect(() => {
if (search) {
if (!localResults || localResults.length === 0) {
lastScrolledIndexRef.current = null;
return;
}
// Only scroll if the active index actually changed
const activeResultIndex = localActiveIndex - 1; // Convert back to 0-based
if (activeResultIndex >= 0 &&
activeResultIndex < localResults.length &&
lastScrolledIndexRef.current !== activeResultIndex) {
const activeResult = localResults[activeResultIndex];
if (activeResult) {
const pageNumber = activeResult.pageIndex + 1; // Convert to 1-based page number
scrollActions.scrollToPage(pageNumber);
lastScrolledIndexRef.current = activeResultIndex;
}
}
}, [localResults, localActiveIndex, scrollActions]);
// Register bridge whenever state changes
useEffect(() => {
const currentSearch = searchRef.current;
if (currentSearch) {
registerBridge('search', {
state: localState,
state: { results: localResults, activeIndex: localActiveIndex },
api: {
search: async (query: string) => {
if (search?.startSearch && search?.searchAllPages) {
search.startSearch();
return search.searchAllPages(query);
// Prevent overlapping searches
if (isSearchingRef.current) {
return null;
}
if (!currentSearch?.startSearch || !currentSearch?.searchAllPages) {
return null;
}
isSearchingRef.current = true;
try {
currentSearch.startSearch();
const results = await currentSearch.searchAllPages(query);
return results;
} catch (error: any) {
// Handle abort errors gracefully - these occur when searches overlap
if (error?.type === 'abort' || error?.message?.includes('abort')) {
// Silently handle abort - this is expected when user types quickly
return null;
}
console.error('Search failed:', error);
return null;
} finally {
isSearchingRef.current = false;
}
},
clear: () => {
try {
if (search?.stopSearch) {
search.stopSearch();
if (currentSearch?.stopSearch) {
currentSearch.stopSearch();
}
} catch (error) {
console.warn('Error stopping search:', error);
@@ -71,21 +149,21 @@ export function SearchAPIBridge() {
},
next: () => {
try {
search?.nextResult?.();
currentSearch?.nextResult?.();
} catch (error) {
console.warn('Error navigating to next result:', error);
}
},
previous: () => {
try {
search?.previousResult?.();
currentSearch?.previousResult?.();
} catch (error) {
console.warn('Error navigating to previous result:', error);
}
},
goToResult: (index: number) => {
try {
search?.goToResult?.(index);
currentSearch?.goToResult?.(index);
} catch (error) {
console.warn('Error going to result:', error);
}
@@ -93,7 +171,7 @@ export function SearchAPIBridge() {
}
});
}
}, [search, localState]);
}, [localResults, localActiveIndex, registerBridge]);
return null;
}
@@ -1,10 +1,7 @@
import { useEffect, useState } from 'react';
import { useSelectionCapability, SelectionRangeX } from '@embedpdf/plugin-selection/react';
import { useSelectionCapability } from '@embedpdf/plugin-selection/react';
import { useViewer } from '@app/contexts/ViewerContext';
/**
* Component that runs inside EmbedPDF context and updates selection state in ViewerContext
*/
export function SelectionAPIBridge() {
const { provides: selection } = useSelectionCapability();
const { registerBridge } = useViewer();
@@ -16,7 +13,6 @@ export function SelectionAPIBridge() {
hasSelection
};
// Register this bridge with ViewerContext
registerBridge('selection', {
state: newState,
api: {
@@ -26,12 +22,10 @@ export function SelectionAPIBridge() {
}
});
// Listen for selection changes to track when text is selected
const unsubscribe = selection.onSelectionChange((sel: SelectionRangeX | null) => {
const hasText = !!sel;
const unsubscribe = selection.onSelectionChange((event: any) => {
const hasText = !!(event?.selection || event);
setHasSelection(hasText);
const updatedState = { hasSelection: hasText };
// Re-register with updated state
registerBridge('selection', {
state: updatedState,
api: {
@@ -1,16 +1,32 @@
import { useEffect } from 'react';
import { useEffect, useRef } from 'react';
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
/**
* Component that runs inside EmbedPDF context and updates spread state in ViewerContext
*/
export function SpreadAPIBridge() {
const { provides: spread, spreadMode } = useSpread();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <SpreadAPIBridgeInner documentId={activeDocumentId} />;
}
function SpreadAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: spread, spreadMode } = useSpread(documentId);
const { registerBridge, triggerImmediateSpreadUpdate } = useViewer();
// Keep spread ref updated to avoid re-running effect when object reference changes
const spreadRef = useRef(spread);
useEffect(() => {
if (!spread) {
spreadRef.current = spread;
}, [spread]);
useEffect(() => {
const currentSpread = spreadRef.current;
if (!currentSpread || spreadMode === undefined) {
return;
}
@@ -23,20 +39,20 @@ export function SpreadAPIBridge() {
state: newState,
api: {
setSpreadMode: (mode: SpreadMode) => {
spread.setSpreadMode(mode);
currentSpread.setSpreadMode(mode);
},
getSpreadMode: () => spread.getSpreadMode(),
getSpreadMode: () => currentSpread.getSpreadMode(),
toggleSpreadMode: () => {
const current = spread.getSpreadMode();
const current = currentSpread.getSpreadMode();
const nextMode = current === SpreadMode.None ? SpreadMode.Odd : SpreadMode.None;
spread.setSpreadMode(nextMode);
currentSpread.setSpreadMode(nextMode);
},
SpreadMode,
},
});
triggerImmediateSpreadUpdate(spreadMode);
}, [spread, spreadMode, registerBridge, triggerImmediateSpreadUpdate]);
}, [spreadMode, registerBridge, triggerImmediateSpreadUpdate]);
return null;
}
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { useZoom, ZoomMode } from '@embedpdf/plugin-zoom/react';
import { useSpread, SpreadMode } from '@embedpdf/plugin-spread/react';
import { useViewer } from '@app/contexts/ViewerContext';
import { useActiveDocumentId } from '@app/components/viewer/useActiveDocumentId';
import { useFileState } from '@app/contexts/FileContext';
import {
determineAutoZoom,
@@ -12,8 +13,19 @@ import {
import { getFirstPageAspectRatioFromStub } from '@app/utils/pageMetadata';
export function ZoomAPIBridge() {
const { provides: zoom, state: zoomState } = useZoom();
const { spreadMode } = useSpread();
const activeDocumentId = useActiveDocumentId();
// Don't render the inner component until we have a valid document ID
if (!activeDocumentId) {
return null;
}
return <ZoomAPIBridgeInner documentId={activeDocumentId} />;
}
function ZoomAPIBridgeInner({ documentId }: { documentId: string }) {
const { provides: zoom, state: zoomState } = useZoom(documentId);
const { spreadMode } = useSpread(documentId);
const { registerBridge, triggerImmediateZoomUpdate } = useViewer();
const { selectors } = useFileState();
@@ -21,8 +33,14 @@ export function ZoomAPIBridge() {
const lastSpreadMode = useRef(spreadMode ?? SpreadMode.None);
const lastFileId = useRef<string | undefined>(undefined);
const lastAppliedZoom = useRef<number | null>(null);
const zoomRef = useRef(zoom);
const [autoZoomTick, setAutoZoomTick] = useState(0);
// Keep zoom ref updated
useEffect(() => {
zoomRef.current = zoom;
}, [zoom]);
const scheduleAutoZoom = useCallback(() => {
hasSetInitialZoom.current = false;
lastAppliedZoom.current = null;
@@ -30,15 +48,22 @@ export function ZoomAPIBridge() {
}, []);
const requestFitWidth = useCallback(() => {
if (zoom) {
zoom.requestZoom(ZoomMode.FitWidth, { vx: 0.5, vy: 0 });
if (zoomRef.current) {
zoomRef.current.requestZoom(ZoomMode.FitWidth, { vx: 0.5, vy: 0 });
}
}, [zoom]);
}, []);
const stubs = selectors.getStirlingFileStubs();
const firstFileStub = stubs[0];
const firstFileId = firstFileStub?.id;
// Extract primitive values from zoomState for dependency arrays
const zoomLevel = zoomState?.zoomLevel;
const currentZoomLevel = zoomState?.currentZoomLevel;
// Extract metadata aspect ratio as a primitive to avoid object reference issues
const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub);
useEffect(() => {
if (!firstFileId) {
hasSetInitialZoom.current = false;
@@ -59,7 +84,6 @@ export function ZoomAPIBridge() {
lastSpreadMode.current = currentSpreadMode;
const hadTrackedAutoZoom = lastAppliedZoom.current !== null;
const zoomLevel = zoomState?.zoomLevel;
if (
zoomLevel === ZoomMode.FitWidth ||
zoomLevel === ZoomMode.Automatic ||
@@ -69,13 +93,13 @@ export function ZoomAPIBridge() {
scheduleAutoZoom();
}
}
}, [spreadMode, zoomState?.zoomLevel, scheduleAutoZoom, requestFitWidth]);
}, [spreadMode, zoomLevel, scheduleAutoZoom, requestFitWidth]);
const isManagedZoom =
!!zoom &&
(zoomState?.zoomLevel === ZoomMode.FitWidth ||
zoomState?.zoomLevel === ZoomMode.Automatic ||
(zoomLevel === ZoomMode.FitWidth ||
zoomLevel === ZoomMode.Automatic ||
lastAppliedZoom.current !== null);
useFitWidthResize({
@@ -85,7 +109,7 @@ export function ZoomAPIBridge() {
});
useEffect(() => {
if (!zoom || !zoomState) {
if (!zoom || zoomLevel === undefined || currentZoomLevel === undefined) {
return;
}
@@ -97,14 +121,14 @@ export function ZoomAPIBridge() {
return;
}
if (zoomState.zoomLevel !== ZoomMode.FitWidth) {
if (zoomState.zoomLevel === ZoomMode.Automatic) {
if (zoomLevel !== ZoomMode.FitWidth) {
if (zoomLevel === ZoomMode.Automatic) {
requestFitWidth();
}
return;
}
const fitWidthZoom = zoomState.currentZoomLevel;
const fitWidthZoom = currentZoomLevel;
if (!fitWidthZoom || fitWidthZoom <= 0 || fitWidthZoom === 1) {
return;
}
@@ -121,7 +145,6 @@ export function ZoomAPIBridge() {
const applyAutoZoom = async () => {
const currentSpreadMode = spreadMode ?? SpreadMode.None;
const pagesPerSpread = currentSpreadMode !== SpreadMode.None ? 2 : 1;
const metadataAspectRatio = getFirstPageAspectRatioFromStub(firstFileStub);
if (cancelled) {
return;
@@ -165,21 +188,31 @@ export function ZoomAPIBridge() {
};
}, [
zoom,
zoomState,
zoomLevel,
currentZoomLevel,
firstFileId,
firstFileStub,
metadataAspectRatio,
requestFitWidth,
autoZoomTick,
spreadMode,
triggerImmediateZoomUpdate,
]);
// Subscribe to zoom changes - use ref to avoid re-subscribing when zoom reference changes
const zoomSubscriptionRef = useRef<(() => void) | null>(null);
useEffect(() => {
// Cleanup previous subscription if any
if (zoomSubscriptionRef.current) {
zoomSubscriptionRef.current();
zoomSubscriptionRef.current = null;
}
if (!zoom) {
return;
}
const unsubscribe = zoom.onZoomChange((event: { newZoom?: number }) => {
zoomSubscriptionRef.current = zoom.onZoomChange((event: { newZoom?: number }) => {
if (typeof event?.newZoom !== 'number') {
return;
}
@@ -188,17 +221,25 @@ export function ZoomAPIBridge() {
});
return () => {
unsubscribe();
if (zoomSubscriptionRef.current) {
zoomSubscriptionRef.current();
zoomSubscriptionRef.current = null;
}
};
}, [zoom, triggerImmediateZoomUpdate]);
// Extract primitive values to avoid dependency on object reference
const zoomStateCurrentZoomLevel = zoomState?.currentZoomLevel;
// Register bridge - only re-run when actual values change
useEffect(() => {
if (!zoom || !zoomState) {
const currentZoom = zoomRef.current;
if (!currentZoom || zoomStateCurrentZoomLevel === undefined) {
return;
}
const currentZoomLevel =
lastAppliedZoom.current ?? zoomState.currentZoomLevel ?? 1;
lastAppliedZoom.current ?? zoomStateCurrentZoomLevel ?? 1;
const newState = {
currentZoom: currentZoomLevel,
@@ -209,9 +250,9 @@ export function ZoomAPIBridge() {
registerBridge('zoom', {
state: newState,
api: zoom,
api: currentZoom,
});
}, [zoom, zoomState, registerBridge, triggerImmediateZoomUpdate]);
}, [zoomStateCurrentZoomLevel, registerBridge, triggerImmediateZoomUpdate]);
return null;
}
@@ -0,0 +1,9 @@
import { useActiveDocument } from '@app/components/viewer/ActiveDocumentContext';
/**
* Hook to get the currently active document ID.
* Uses a shared context to avoid multiple subscriptions.
*/
export function useActiveDocumentId(): string | null {
return useActiveDocument();
}
@@ -38,6 +38,8 @@ import {
ExportState,
ThumbnailAPIWrapper,
BookmarkState,
DocumentPermissionsState,
PdfPermissionFlag,
} from '@app/contexts/viewer/viewerBridges';
import { SpreadMode } from '@embedpdf/plugin-spread/react';
@@ -112,6 +114,8 @@ interface ViewerContextType {
getExportState: () => ExportState;
getBookmarkState: () => BookmarkState;
hasBookmarkSupport: () => boolean;
getDocumentPermissions: () => DocumentPermissionsState;
hasPermission: (flag: PdfPermissionFlag) => boolean;
// Immediate update callbacks
registerImmediateZoomUpdate: (callback: (percent: number) => void) => () => void;
@@ -308,6 +312,34 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
const hasBookmarkSupport = () => Boolean(bridgeRefs.current.bookmark);
const getDocumentPermissions = (): DocumentPermissionsState => {
return bridgeRefs.current.permissions?.state || {
isEncrypted: false,
isOwnerUnlocked: false,
permissions: PdfPermissionFlag.AllowAll,
canPrint: true,
canModifyContents: true,
canCopyContents: true,
canModifyAnnotations: true,
canFillForms: true,
canExtractForAccessibility: true,
canAssembleDocument: true,
canPrintHighQuality: true,
};
};
const hasPermission = (flag: PdfPermissionFlag): boolean => {
const api = bridgeRefs.current.permissions?.api;
if (api?.hasPermission) {
return api.hasPermission(flag);
}
// Default: allow all permissions - warn in development
if (process.env.NODE_ENV === 'development') {
console.warn('[ViewerContext] Permissions API not available, defaulting to allow');
}
return true;
};
// Action handlers - call APIs directly
const {
scrollActions,
@@ -360,6 +392,8 @@ export const ViewerProvider: React.FC<ViewerProviderProps> = ({ children }) => {
getExportState,
getBookmarkState,
hasBookmarkSupport,
getDocumentPermissions,
hasPermission,
// Immediate updates
registerImmediateZoomUpdate,
@@ -100,32 +100,63 @@ export function createViewerActions({
scrollToPage: (page: number, behavior?: 'smooth' | 'instant') => {
const api = registry.current.scroll?.api;
if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: page, behavior: behavior || 'smooth' });
try {
api.scrollToPage({ pageNumber: page, behavior: behavior || 'smooth' });
} catch (error) {
// Silently handle "Strategy not found" errors that occur during document transitions
if (process.env.NODE_ENV === 'development') {
console.warn('[ScrollActions] scrollToPage failed (document may be transitioning):', error);
}
}
}
},
scrollToFirstPage: () => {
const api = registry.current.scroll?.api;
if (api?.scrollToPage) {
api.scrollToPage({ pageNumber: 1 });
try {
api.scrollToPage({ pageNumber: 1 });
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ScrollActions] scrollToFirstPage failed:', error);
}
}
}
},
scrollToPreviousPage: () => {
const api = registry.current.scroll?.api;
if (api?.scrollToPreviousPage) {
api.scrollToPreviousPage();
try {
api.scrollToPreviousPage();
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ScrollActions] scrollToPreviousPage failed:', error);
}
}
}
},
scrollToNextPage: () => {
const api = registry.current.scroll?.api;
if (api?.scrollToNextPage) {
api.scrollToNextPage();
try {
api.scrollToNextPage();
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ScrollActions] scrollToNextPage failed:', error);
}
}
}
},
scrollToLastPage: () => {
const api = registry.current.scroll?.api;
const state = getScrollState();
if (api?.scrollToPage && state.totalPages > 0) {
api.scrollToPage({ pageNumber: state.totalPages });
try {
api.scrollToPage({ pageNumber: state.totalPages });
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.warn('[ScrollActions] scrollToLastPage failed:', error);
}
}
}
},
};
@@ -1,6 +1,38 @@
import { SpreadMode } from '@embedpdf/plugin-spread/react';
import { PdfBookmarkObject } from '@embedpdf/models';
export enum PdfPermissionFlag {
Print = 0x0004,
ModifyContents = 0x0008,
CopyContents = 0x0010,
ModifyAnnotations = 0x0020,
FillForms = 0x0100,
ExtractForAccessibility = 0x0200,
AssembleDocument = 0x0400,
PrintHighQuality = 0x0800,
AllowAll = 0x0f3c,
}
export interface DocumentPermissionsState {
isEncrypted: boolean;
isOwnerUnlocked: boolean;
permissions: number;
canPrint: boolean;
canModifyContents: boolean;
canCopyContents: boolean;
canModifyAnnotations: boolean;
canFillForms: boolean;
canExtractForAccessibility: boolean;
canAssembleDocument: boolean;
canPrintHighQuality: boolean;
}
export interface DocumentPermissionsAPIWrapper {
hasPermission: (flag: PdfPermissionFlag) => boolean;
hasAllPermissions: (flags: PdfPermissionFlag[]) => boolean;
getEffectivePermission: (flag: PdfPermissionFlag) => boolean;
}
export interface ScrollAPIWrapper {
scrollToPage: (params: { pageNumber: number; behavior?: ScrollBehavior }) => void;
scrollToPreviousPage: () => void;
@@ -137,6 +169,7 @@ export interface BridgeStateMap {
export: ExportState;
bookmark: BookmarkState;
print: unknown;
permissions: DocumentPermissionsState;
}
export interface BridgeApiMap {
@@ -151,6 +184,7 @@ export interface BridgeApiMap {
export: ExportAPIWrapper;
bookmark: BookmarkAPIWrapper;
print: PrintAPIWrapper;
permissions: DocumentPermissionsAPIWrapper;
}
export type BridgeKey = keyof BridgeStateMap;
@@ -171,6 +205,7 @@ export const createBridgeRegistry = (): ViewerBridgeRegistry => ({
export: null,
bookmark: null,
print: null,
permissions: null,
});
export function registerBridge<K extends BridgeKey>(
@@ -38,7 +38,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
const BASE_NO_LOGIN_CONFIG: AppConfig = {
enableAnalytics: true,
appVersion: '2.4.0',
appVersion: '2.4.1',
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
serverPort: 8080,
@@ -270,7 +270,11 @@ export function useAnnotationSelection({
// internal selection state (e.g., accessing `selectedUid` on
// an undefined object). Treat this as "no current selection"
// instead of crashing the annotations tool.
console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error);
// Only log unexpected errors - "No active document" is a common expected state during init
const errorMessage = error instanceof Error ? error.message : String(error);
if (!errorMessage.includes('No active document')) {
console.error('[useAnnotationSelection] getSelectedAnnotation failed:', error);
}
ann = null;
}
}
+51 -9
View File
@@ -3,6 +3,8 @@
*/
import { AutomationConfig } from '@app/types/automation';
import { ToolRegistry } from '@app/data/toolsTaxonomy';
import { ToolId } from '@app/types/toolId';
/**
* Pipeline configuration format used by folder scanning
@@ -24,18 +26,54 @@ interface FolderScanningPipeline {
/**
* Converts an AutomationConfig to a folder scanning pipeline configuration
* @param automation The automation configuration to convert
* @param toolRegistry The tool registry to map operation types to endpoints
* @returns Folder scanning pipeline configuration
*/
export function convertToFolderScanningConfig(automation: AutomationConfig): FolderScanningPipeline {
export function convertToFolderScanningConfig(
automation: AutomationConfig,
toolRegistry: Partial<ToolRegistry>
): FolderScanningPipeline {
return {
name: automation.name,
pipeline: automation.operations.map(op => ({
operation: op.operation,
parameters: {
...op.parameters,
fileInput: "automated"
pipeline: automation.operations.map(op => {
// Map operationType to full API endpoint path
const toolId = op.operation as ToolId;
const toolEntry = toolRegistry[toolId];
const endpointConfig = toolEntry?.operationConfig?.endpoint;
let endpoint: string | undefined;
if (typeof endpointConfig === 'string') {
endpoint = endpointConfig;
} else if (typeof endpointConfig === 'function') {
// For dynamic endpoints, call with the saved parameters
try {
endpoint = endpointConfig(op.parameters);
} catch (error) {
console.warn(
`Failed to resolve dynamic endpoint for operation "${op.operation}". ` +
`This may happen if the tool requires specific parameters. ` +
`Error: ${error}`
);
}
}
})),
if (!endpoint) {
console.warn(
`No endpoint found for operation "${op.operation}". ` +
`This operation may fail in folder scanning. ` +
`Using operation type as fallback.`
);
}
return {
operation: endpoint || op.operation,
parameters: {
...op.parameters,
fileInput: "automated"
}
};
}),
_examples: {
outputDir: "{outputFolder}/{folderName}",
outputFileName: "{filename}-{pipelineName}-{date}-{time}"
@@ -48,9 +86,13 @@ export function convertToFolderScanningConfig(automation: AutomationConfig): Fol
/**
* Downloads a folder scanning configuration as a JSON file
* @param automation The automation configuration to export
* @param toolRegistry The tool registry to map operation types to endpoints
*/
export function downloadFolderScanningConfig(automation: AutomationConfig): void {
const config = convertToFolderScanningConfig(automation);
export function downloadFolderScanningConfig(
automation: AutomationConfig,
toolRegistry: Partial<ToolRegistry>
): void {
const config = convertToFolderScanningConfig(automation, toolRegistry);
const json = JSON.stringify(config, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
+12 -2
View File
@@ -8,16 +8,26 @@ export const getSynonyms = (t: TFunction, toolId: string): string[] => {
// If the translation key doesn't exist or returns the key itself, return empty array
if (!tags || tags === tagsKey) {
console.warn(`[Tags] Missing tags for tool: ${toolId}`);
return [];
}
// Split by comma and clean up the tags
return tags
const cleanedTags = tags
.split(',')
.map((tag: string) => tag.trim())
.filter((tag: string) => tag.length > 0);
// Log the tags found for this tool
if (cleanedTags.length > 0) {
console.info(`[Tags] Tool "${toolId}" has ${cleanedTags.length} tags:`, cleanedTags);
} else {
console.warn(`[Tags] Tool "${toolId}" has empty tags value`);
}
return cleanedTags;
} catch (error) {
console.warn(`Failed to get translated synonyms for tool ${toolId}:`, error);
console.error(`[Tags] Failed to get translated synonyms for tool ${toolId}:`, error);
return [];
}};
@@ -69,11 +69,28 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
console.log('[ServerSelection] ✅ Connection test successful');
// Check if the connection requires accepting invalid certificates
const allowInvalidCerts = testResult.requiresInvalidCertAcceptance || false;
if (allowInvalidCerts) {
console.warn('[ServerSelection] ⚠️ Server uses self-signed/invalid certificate - will accept for all requests');
}
// Fetch OAuth providers and check if login is enabled
const enabledProviders: SSOProviderConfig[] = [];
try {
console.log('[ServerSelection] Fetching login configuration...');
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`);
// Prepare fetch options with danger mode if needed
const fetchOptions: any = {};
if (allowInvalidCerts) {
fetchOptions.danger = {
acceptInvalidCerts: true,
acceptInvalidHostnames: true,
};
console.debug('[ServerSelection] Using danger mode for login config fetch');
}
const response = await fetch(`${url}/api/v1/proprietary/ui-data/login`, fetchOptions);
// Check if security is disabled (status 403, 401, or 404 - endpoint doesn't exist)
if (!response.ok) {
@@ -147,11 +164,12 @@ export const ServerSelection: React.FC<ServerSelectionProps> = ({ onSelect, load
return;
}
// Connection successful - pass URL and OAuth providers
// Connection successful - pass URL, OAuth providers, and cert settings
console.log('[ServerSelection] ✅ Server selection complete, proceeding to login');
onSelect({
url,
enabledOAuthProviders: enabledProviders.length > 0 ? enabledProviders : undefined,
allow_invalid_certs: allowInvalidCerts,
});
} catch (error) {
console.error('[ServerSelection] ❌ Unexpected error during connection test:', error);
@@ -45,6 +45,8 @@ export function setupApiInterceptors(client: AxiosInstance): void {
extendedConfig.url = `${baseUrl}${extendedConfig.url}`;
}
localStorage.setItem('server_url', baseUrl);
// Debug logging
console.debug(`[apiClientSetup] Request to: ${extendedConfig.url}`);
@@ -12,6 +12,7 @@ export interface SSOProviderConfig {
export interface ServerConfig {
url: string;
enabledOAuthProviders?: SSOProviderConfig[];
allow_invalid_certs?: boolean;
}
export interface ConnectionConfig {
@@ -31,6 +32,7 @@ export interface ConnectionTestResult {
error?: string;
errorCode?: string;
diagnostics?: DiagnosticResult[];
requiresInvalidCertAcceptance?: boolean;
}
export class ConnectionModeService {
@@ -251,11 +253,16 @@ export class ConnectionModeService {
diagnostics.push(stage2Result);
if (stage2Result.success) {
console.log(`[ConnectionModeService] ⚠️ Certificate issue detected - works without validation`);
console.log(`[ConnectionModeService] ⚠️ Certificate issue detected - but connection works with bypass enabled`);
console.log(`[ConnectionModeService] ==================== DIAGNOSTIC SUMMARY ====================`);
console.log(`[ConnectionModeService] ✅ CONNECTION SUCCESSFUL (with certificate bypass)`);
console.log(`[ConnectionModeService] Protocol: HTTPS with certificate validation disabled`);
console.log(`[ConnectionModeService] Duration: ${stage2Result.duration}ms`);
console.log(`[ConnectionModeService] Note: Server has self-signed or invalid certificate - allowing connection`);
console.log(`[ConnectionModeService] ==================== DIAGNOSTIC SESSION END ====================`);
return {
success: false,
error: 'SSL certificate validation failed. The server has an invalid, self-signed, or untrusted certificate.',
errorCode: 'SSL_CERTIFICATE_INVALID',
success: true,
requiresInvalidCertAcceptance: true,
diagnostics,
};
}
@@ -487,7 +494,13 @@ export class ConnectionModeService {
let detailedMessage = `Failed: ${errorMsg}`;
if (errorLower.includes('timeout') || errorLower.includes('timed out')) {
// Check for TLS version mismatch (TLS 1.0/1.1 not supported)
if (errorLower.includes('peer is incompatible') ||
errorLower.includes('protocol version') ||
errorLower.includes('peerincompatible') ||
(errorLower.includes('handshake') && (errorLower.includes('tls') || errorLower.includes('ssl')))) {
detailedMessage = `TLS version not supported - Server appears to use TLS 1.0 or 1.1 (desktop app requires TLS 1.2+). Please upgrade your server's TLS configuration or use the web version.`;
} else if (errorLower.includes('timeout') || errorLower.includes('timed out')) {
detailedMessage = `Timeout after ${duration}ms - server not responding`;
} else if (errorLower.includes('certificate') || errorLower.includes('cert') || errorLower.includes('ssl') || errorLower.includes('tls')) {
detailedMessage = `SSL/TLS error - ${errorMsg}`;
@@ -193,12 +193,26 @@ class TauriHttpClient {
const credentials: RequestCredentials = finalConfig.withCredentials ? 'include' : 'omit';
// Make the request using Tauri's native HTTP client (standard Fetch API)
const response = await fetch(url, {
// Enable certificate bypass for HTTPS to handle missing intermediate certs and self-signed certs
const fetchOptions: any = {
method,
headers,
body,
credentials,
});
};
// Always enable dangerous settings for HTTPS to allow connections to servers with:
// - Missing intermediate certificates
// - Self-signed certificates
// - Certificate hostname mismatches
if (url.startsWith('https://')) {
fetchOptions.danger = {
acceptInvalidCerts: true,
acceptInvalidHostnames: true,
};
}
const response = await fetch(url, fetchOptions);
// Parse response based on responseType
let data: T;
@@ -16,6 +16,7 @@ interface SecuritySettingsData {
loginMethod?: string;
loginAttemptCount?: number;
loginResetTimeMinutes?: number;
xFrameOptions?: string;
jwt?: {
persistence?: boolean;
enableKeyRotation?: boolean;
@@ -125,6 +126,7 @@ export default function AdminSecuritySection() {
'security.loginMethod': securitySettings.loginMethod,
'security.loginAttemptCount': securitySettings.loginAttemptCount,
'security.loginResetTimeMinutes': securitySettings.loginResetTimeMinutes,
'security.xFrameOptions': securitySettings.xFrameOptions,
// JWT settings
'security.jwt.persistence': securitySettings.jwt?.persistence,
'security.jwt.enableKeyRotation': securitySettings.jwt?.enableKeyRotation,
@@ -280,6 +282,27 @@ export default function AdminSecuritySection() {
disabled={!loginEnabled}
/>
</div>
<div>
<Select
label={
<Group gap="xs">
<span>{t('admin.settings.security.xFrameOptions.label', 'X-Frame-Options')}</span>
<PendingBadge show={isFieldPending('xFrameOptions')} />
</Group>
}
description={t('admin.settings.security.xFrameOptions.description', 'Controls whether the application can be embedded in iframes')}
value={settings?.xFrameOptions || 'DENY'}
onChange={(value) => setSettings({ ...settings, xFrameOptions: value || 'DENY' })}
data={[
{ value: 'DENY', label: t('admin.settings.security.xFrameOptions.deny', 'Deny (Prevents all framing)') },
{ value: 'SAMEORIGIN', label: t('admin.settings.security.xFrameOptions.sameorigin', 'Same Origin (Allow framing from same domain)') },
{ value: 'DISABLED', label: t('admin.settings.security.xFrameOptions.disabled', 'Disabled (No X-Frame-Options header)') },
]}
comboboxProps={{ zIndex: 1400 }}
disabled={!loginEnabled}
/>
</div>
</Stack>
</Paper>
@@ -48,7 +48,7 @@ const FREE_LICENSE_INFO: LicenseInfo = {
const BASE_NO_LOGIN_CONFIG: AppConfig = {
enableAnalytics: true,
appVersion: '2.4.0',
appVersion: '2.4.1',
serverCertificateEnabled: false,
enableAlphaFunctionality: false,
enableDesktopInstallSlide: true,
+3 -3
View File
@@ -291,9 +291,9 @@ pycryptodome==3.23.0 \
--hash=sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be \
--hash=sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7
# via -r requirements.in
pypdf==6.6.0 \
--hash=sha256:4c887ef2ea38d86faded61141995a3c7d068c9d6ae8477be7ae5de8a8e16592f \
--hash=sha256:bca9091ef6de36c7b1a81e09327c554b7ce51e88dad68f5890c2b4a4417f1fd7
pypdf==6.6.2 \
--hash=sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016 \
--hash=sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba
# via -r requirements.in
reportlab==4.4.9 \
--hash=sha256:68e2d103ae8041a37714e8896ec9b79a1c1e911d68c3bd2ea17546568cf17bfd \