Compare commits

...

103 Commits

Author SHA1 Message Date
Anthony Stirling
60ced19f64 Update build.yml 2024-12-16 20:59:47 +00:00
Anthony Stirling
63eb94c0a6 Merge pull request #2463 from reecebrowne/bug/fix_merge
Add missing pdflib
2024-12-16 14:11:36 +00:00
Reece Browne
0a6b6453b2 Update build number 2024-12-16 14:03:38 +00:00
Reece Browne
2931e348a9 Add missing pdflib 2024-12-16 13:55:44 +00:00
Anthony Stirling
c71ca21532 Update multiOSReleases.yml 2024-12-14 11:08:35 +00:00
Anthony Stirling
0e4c3d5dbc Update releaseArtifacts.yml 2024-12-14 11:01:49 +00:00
Anthony Stirling
92cabf125e Merge pull request #2453 from Stirling-Tools/csrf2
Csrf fixes
2024-12-14 10:59:50 +00:00
Anthony Stirling
818bed3154 Update build.gradle 2024-12-14 10:43:35 +00:00
Anthony Stirling
1f1c414138 csrf fixes 2024-12-14 10:42:07 +00:00
Anthony Stirling
f1c5384a37 Merge pull request #2445 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-13 20:33:21 +00:00
Anthony Stirling
5607f7079e Merge pull request #2441 from reecebrowne/bug/csrf-decryption-API
CSRF token for decryption
2024-12-13 20:33:03 +00:00
Anthony Stirling
3b8723975d Merge branch 'main' into bug/csrf-decryption-API 2024-12-13 20:31:49 +00:00
Anthony Stirling
41a3d28c90 Merge pull request #2439 from Stirling-Tools/update-3rd-party-licenses
Update 3rd Party Licenses
2024-12-13 20:31:23 +00:00
GitHub Action
f7afe73cb4 Update 3rd Party Licenses
Signed-off-by: GitHub Action <action@github.com>
2024-12-13 20:26:32 +00:00
github-actions[bot]
73e5246191 📝 Sync README
> Made via sync_files.yml
2024-12-13 20:25:58 +00:00
Anthony Stirling
1f39481efe Merge branch 'main' into bug/csrf-decryption-API 2024-12-13 20:25:52 +00:00
Anthony Stirling
5ac2260d78 Merge pull request #2451 from Stirling-Tools/testStuff Full local webUI client
Full local webUI client
2024-12-13 20:25:39 +00:00
Anthony Stirling
bae83a281c Update Dockerfile 2024-12-13 20:07:45 +00:00
Anthony Stirling
dd2aae60ad Update Dockerfile-fat 2024-12-13 20:07:30 +00:00
Anthony Stirling
30ee33002d Merge branch 'main' into testStuff 2024-12-13 19:31:53 +00:00
Anthony Stirling
24717dde19 finish 2024-12-13 18:20:54 +00:00
Anthony Stirling
509a305985 logs and cleanup 2024-12-13 16:58:34 +00:00
Anthony Stirling
13572a7f18 remove non windows for now 2024-12-13 16:04:45 +00:00
Anthony Stirling
ebd0ddc6ad test 2024-12-13 12:14:21 +00:00
Anthony Stirling
43c4ec1089 fixes! 2024-12-13 11:31:49 +00:00
Anthony Stirling
1ccdc1697b dif main class 2024-12-13 10:49:04 +00:00
Anthony Stirling
2297c5dc95 Merge pull request #2442 from lhui/main
add and refactor CN translate
2024-12-13 10:29:13 +00:00
Anthony Stirling
859c9942e6 Merge pull request #2444 from tkymmm/main
Update messages_ja_JP.properties
2024-12-13 10:28:57 +00:00
Anthony Stirling
c723696bd0 Merge pull request #2443 from albanobattistella/patch-61
Update messages_it_IT.properties
2024-12-13 10:28:46 +00:00
Anthony Stirling
fe198458ca fix main class 2024-12-13 10:28:03 +00:00
tkymmm
378aca4460 Update messages_ja_JP.properties 2024-12-13 13:23:26 +09:00
Anthony Stirling
9870e6ad7c fix 2024-12-13 00:59:42 +00:00
Anthony Stirling
40b5904726 test 2024-12-13 00:52:12 +00:00
Anthony Stirling
de3f59cf44 destination = "${projectDir}/build/jpackage" 2024-12-13 00:47:37 +00:00
Anthony Stirling
4ae7c83357 tests 2024-12-13 00:44:32 +00:00
Anthony Stirling
f127271709 test 2024-12-13 00:31:39 +00:00
Anthony Stirling
f899088c75 test 2024-12-13 00:29:03 +00:00
Anthony Stirling
d55e007f93 test dirs 2024-12-13 00:24:51 +00:00
Anthony Stirling
ccdcb05e65 x test 2024-12-13 00:13:47 +00:00
Anthony Stirling
e6c2ad8c9c test 2024-12-13 00:00:56 +00:00
Anthony Stirling
32aa623c8b test 2024-12-12 23:57:12 +00:00
Anthony Stirling
23888c5d2c test 2024-12-12 23:50:51 +00:00
Anthony Stirling
2f23eb69c6 naming 2024-12-12 23:40:49 +00:00
Anthony Stirling
446cedf26e unix name 2024-12-12 23:26:23 +00:00
Anthony Stirling
f950e25dad versioning 2024-12-12 23:22:19 +00:00
Anthony Stirling
65a86a6e97 remove mac 18+ 2024-12-12 23:16:27 +00:00
Anthony Stirling
7f4134f52d info 2024-12-12 23:08:28 +00:00
Anthony Stirling
55969697b8 build 2024-12-12 23:05:59 +00:00
Anthony Stirling
86662d9cf6 more testing 2024-12-12 23:03:42 +00:00
albanobattistella
2437460c73 Update messages_it_IT.properties 2024-12-12 19:00:26 +01:00
lihui
a7960d992c add and refactor CN translate 2024-12-13 00:42:18 +08:00
Reece Browne
c98cd8117f CSRF token for decryption 2024-12-12 13:07:50 +00:00
Anthony Stirling
50c5efac87 Merge pull request #2440 from MaratheHarshad/fix/collapsible-menus-on-reload
Fix collapsed menu issue on page reload
2024-12-12 11:48:54 +00:00
Anthony Stirling
0952245b23 Merge pull request #2425 from Stirling-Tools/dependabot/docker/alpine-3.21.0
Bump alpine from 3.20.3 to 3.21.0
2024-12-12 11:32:16 +00:00
dependabot[bot]
83ddfdf152 Bump alpine from 3.20.3 to 3.21.0
Bumps alpine from 3.20.3 to 3.21.0.

---
updated-dependencies:
- dependency-name: alpine
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-12 11:29:57 +00:00
Anthony Stirling
08777c78b1 Merge pull request #2423 from Stirling-Tools/dependabot/gradle/org.thymeleaf.extras-thymeleaf-extras-springsecurity5-3.1.3.RELEASE
Bump org.thymeleaf.extras:thymeleaf-extras-springsecurity5 from 3.1.2.RELEASE to 3.1.3.RELEASE
2024-12-12 11:29:22 +00:00
Anthony Stirling
c6980e9693 Merge pull request #2434 from Ludy87/security_fix_1
Security fix: Server-Side Request Forgery
2024-12-12 11:28:56 +00:00
Anthony Stirling
fc514ee65b Merge pull request #2436 from Stirling-Tools/snyk-fix-9e2683c5d747da5ff97f3f913479b288
[Snyk] Security upgrade alpine from 3.20.3 to 3.21.0
2024-12-12 11:28:37 +00:00
Harshad Marathe
44be2b99d2 Fix collapsed menu after reload on state closed 2024-12-12 16:55:40 +05:30
Anthony Stirling
549824c91f Merge pull request #2438 from Stirling-Tools/sync_readme
📝 Update README: Translation Progress Table
2024-12-12 11:08:31 +00:00
github-actions[bot]
1c0d1efda6 📝 Sync README
> Made via sync_files.yml
2024-12-12 11:01:38 +00:00
Anthony Stirling
9d8d90bf2f Merge pull request #2432 from Stirling-Tools/update_translation_files
Update translation files
2024-12-12 11:01:22 +00:00
snyk-bot
a7e7d57b23 fix: Dockerfile-ultra-lite to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201
- https://snyk.io/vuln/SNYK-ALPINE320-OPENSSL-8235201
2024-12-12 00:48:00 +00:00
Anthony Stirling
e014bb023b fixes 2024-12-11 23:21:56 +00:00
Anthony Stirling
1c5dfc46a0 fixes 2024-12-11 23:13:23 +00:00
Anthony Stirling
11273b1589 fix gha release 2024-12-11 22:29:58 +00:00
Anthony Stirling
dd5af46906 gha fix 2024-12-11 22:17:27 +00:00
Anthony Stirling
c20d37518d prop fixes 2024-12-11 22:09:35 +00:00
Anthony Stirling
eb20f51958 headless 2024-12-11 21:56:50 +00:00
Anthony Stirling
97d28ac6d2 Windows UI .exe 2024-12-11 21:54:05 +00:00
Anthony Stirling
026fe8150d Merge pull request #2427 from Stirling-Tools/testStuff
X-API-key to X-API-KEY and enable CSRF protection for all users
2024-12-11 21:52:57 +00:00
Ludy87
c3f88f716c Update GeneralUtils.java 2024-12-11 21:10:18 +01:00
Ludy87
67f983f00d Security fix: Server-Side Request Forgery
https://github.com/Stirling-Tools/Stirling-PDF/security/advisories/GHSA-4v4c-9hpr-93vx
2024-12-11 21:06:07 +01:00
github-actions[bot]
9167f12296 Update translation files
Signed-off-by: GitHub Action <action@github.com>
2024-12-11 17:28:28 +00:00
Anthony Stirling
93e190fdeb Merge pull request #2412 from reecebrowne/feature/1856/decrypt
Feature/1856/decrypt
2024-12-11 17:27:28 +00:00
Anthony Stirling
82bebf5c62 Merge branch 'main' into feature/1856/decrypt 2024-12-11 17:26:14 +00:00
Reece Browne
bb3f076e6d Final pages fixed for decryption 2024-12-11 16:55:27 +00:00
Reece Browne
64dfa4b841 Tweak additional files to integrate decryption and clean up js 2024-12-10 22:21:00 +00:00
thiagoor-cpu
0f6f3f305a Update messages_pt_BR.properties (#2429)
Up-to-date translation PT-BR
2024-12-10 20:40:28 +00:00
Anthony Stirling
58c7d7b9a8 X-API-key to X-API-KEY 2024-12-10 20:39:24 +00:00
Reece Browne
ef8231de3a Add Decrypt to all relevant pages 2024-12-10 16:39:06 +00:00
Anthony Stirling
c1c3eba398 ensure csrf is enabled 2024-12-10 11:17:50 +00:00
dependabot[bot]
52693541d9 Bump org.thymeleaf.extras:thymeleaf-extras-springsecurity5
Bumps org.thymeleaf.extras:thymeleaf-extras-springsecurity5 from 3.1.2.RELEASE to 3.1.3.RELEASE.

---
updated-dependencies:
- dependency-name: org.thymeleaf.extras:thymeleaf-extras-springsecurity5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-09 22:44:23 +00:00
Anthony Stirling
1639e0fc4c format 2024-12-09 20:41:13 +00:00
Anthony Stirling
0652299bec fixes 2024-12-09 20:40:59 +00:00
Reece Browne
1d6511b043 Check if file is encrypted without password 2024-12-09 13:20:08 +00:00
F43Z
a400fe6015 feat: fa locale added (#2416)
* feat: fa locale added

* fix: fa_locale issues fixed

* Update settings.json
2024-12-08 20:19:52 +00:00
Sai Kumar
b47df3d252 Text color selection for watermark (#2415)
* added custom color selection for Watermark

* using the same translation as AddStampRequest.customColor for the new watermark.customColor

* fixed the space issue between words
2024-12-07 14:19:50 +00:00
Michael Bohn
cb6e1cd94e Update messages_de_DE.properties (#2410)
Some minor changes/fixes to german translation.
2024-12-07 12:15:52 +00:00
Reece Browne
6ee6254f5a Additional decryption translations 2024-12-06 21:26:28 +00:00
Reece Browne
f2c9549ba1 Password prompt translations 2024-12-06 20:53:16 +00:00
Reece Browne
58278c07ff Translations for errors 2024-12-06 20:46:04 +00:00
Reece Browne
4d017610b8 PDF decryption 2024-12-06 19:08:18 +00:00
Reece Browne
dcafc0d487 Merge branch 'decrypt' of https://github.com/Stirling-Tools/Stirling-PDF 2024-12-06 15:33:44 +00:00
github-actions[bot]
2ec8c97737 📝 Update README: Translation Progress Table (#2409)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 12:27:14 +00:00
Anthony Stirling
397a07afe8 english translation fixes (#2408)
* english

* Update messages_en_GB.properties
2024-12-06 12:25:08 +00:00
github-actions[bot]
b072c39fd9 📝 Update README: Translation Progress Table (#2406)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 10:28:05 +00:00
Ludy
1bc6b4149c Update German translation + remove unnecessary comment (#2405) 2024-12-06 10:23:00 +00:00
Anthony Stirling
5a5a8bb7ba quick fixes (#2404) 2024-12-05 19:55:34 +00:00
github-actions[bot]
b6eca59f23 📝 Update README: Translation Progress Table (#2402)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-05 19:54:34 +00:00
Anthony Stirling
f4082e3f96 Update PasswordController.java 2024-07-07 22:51:59 +01:00
Anthony Stirling
c93a48b40d Merge branch 'main' into decrypt 2024-07-07 22:50:41 +01:00
Anthony Stirling
75e10efcbd auto decrypt, update discord, fix multi file support for some inputs 2024-07-07 22:49:21 +01:00
120 changed files with 5949 additions and 2474 deletions

View File

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

View File

@@ -76,7 +76,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.7"
python-version: "3.12"
- name: Pip requirements
run: |

96
.github/workflows/multiOSReleases.yml vendored Normal file
View File

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<h1 align="center">Stirling-PDF</h1>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/HYmhKj45pU)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf)
@@ -19,7 +19,7 @@ All files and PDFs exist either exclusively on the client side, reside in server
## Features
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Enterprise features like SSO Check [here](https://docs.stirlingpdf.com/Enterprise%20Edition)
- Dark mode support
- Custom download options
- Parallel file processing and downloads
@@ -187,46 +187,47 @@ Certain functionality like `Sign` supports pre-saved files stored at `/customFil
## Supported Languages
Stirling-PDF currently supports 37 languages!
Stirling-PDF currently supports 38 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![95%](https://geps.dev/progress/95) |
| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![93%](https://geps.dev/progress/93) |
| Basque (Euskara) (eu_ES) | ![52%](https://geps.dev/progress/52) |
| Basque (Euskara) (eu_ES) | ![53%](https://geps.dev/progress/53) |
| Bulgarian (Български) (bg_BG) | ![90%](https://geps.dev/progress/90) |
| Catalan (Català) (ca_CA) | ![84%](https://geps.dev/progress/84) |
| Croatian (Hrvatski) (hr_HR) | ![92%](https://geps.dev/progress/92) |
| Croatian (Hrvatski) (hr_HR) | ![91%](https://geps.dev/progress/91) |
| Czech (Česky) (cs_CZ) | ![91%](https://geps.dev/progress/91) |
| Danish (Dansk) (da_DK) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![90%](https://geps.dev/progress/90) |
| Dutch (Nederlands) (nl_NL) | ![89%](https://geps.dev/progress/89) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![93%](https://geps.dev/progress/93) |
| German (Deutsch) (de_DE) | ![93%](https://geps.dev/progress/93) |
| Greek (Ελληνικά) (el_GR) | ![91%](https://geps.dev/progress/91) |
| Hindi (हिंदी) (hi_IN) | ![89%](https://geps.dev/progress/89) |
| Hungarian (Magyar) (hu_HU) | ![92%](https://geps.dev/progress/92) |
| French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![90%](https://geps.dev/progress/90) |
| Hindi (हिंदी) (hi_IN) | ![88%](https://geps.dev/progress/88) |
| Hungarian (Magyar) (hu_HU) | ![91%](https://geps.dev/progress/91) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![91%](https://geps.dev/progress/91) |
| Irish (Gaeilge) (ga_IE) | ![82%](https://geps.dev/progress/82) |
| Italian (Italiano) (it_IT) | ![95%](https://geps.dev/progress/95) |
| Japanese (日本語) (ja_JP) | ![80%](https://geps.dev/progress/80) |
| Irish (Gaeilge) (ga_IE) | ![83%](https://geps.dev/progress/83) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![93%](https://geps.dev/progress/93) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Norwegian (Norsk) (no_NB) | ![82%](https://geps.dev/progress/82) |
| Polish (Polski) (pl_PL) | ![91%](https://geps.dev/progress/91) |
| Persian (فارسی) (fa_IR) | ![99%](https://geps.dev/progress/99) |
| Polish (Polski) (pl_PL) | ![90%](https://geps.dev/progress/90) |
| Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) |
| Portuguese Brazilian (Português) (pt_BR) | ![92%](https://geps.dev/progress/92) |
| Romanian (Română) (ro_RO) | ![84%](https://geps.dev/progress/84) |
| Russian (Русский) (ru_RU) | ![91%](https://geps.dev/progress/91) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![66%](https://geps.dev/progress/66) |
| Simplified Chinese (简体中文) (zh_CN) | ![85%](https://geps.dev/progress/85) |
| Slovakian (Slovensky) (sk_SK) | ![77%](https://geps.dev/progress/77) |
| Spanish (Español) (es_ES) | ![92%](https://geps.dev/progress/92) |
| Swedish (Svenska) (sv_SE) | ![91%](https://geps.dev/progress/91) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![85%](https://geps.dev/progress/85) |
| Russian (Русский) (ru_RU) | ![90%](https://geps.dev/progress/90) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![67%](https://geps.dev/progress/67) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![78%](https://geps.dev/progress/78) |
| Spanish (Español) (es_ES) | ![91%](https://geps.dev/progress/91) |
| Swedish (Svenska) (sv_SE) | ![90%](https://geps.dev/progress/90) |
| Thai (ไทย) (th_TH) | ![90%](https://geps.dev/progress/90) |
| Traditional Chinese (繁體中文) (zh_TW) | ![92%](https://geps.dev/progress/92) |
| Traditional Chinese (繁體中文) (zh_TW) | ![91%](https://geps.dev/progress/91) |
| Turkish (Türkçe) (tr_TR) | ![86%](https://geps.dev/progress/86) |
| Ukrainian (Українська) (uk_UA) | ![75%](https://geps.dev/progress/75) |
| Ukrainian (Українська) (uk_UA) | ![76%](https://geps.dev/progress/76) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![83%](https://geps.dev/progress/83) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)
@@ -240,7 +241,7 @@ Stirling PDF offers a Enterprise edition of its software, This is the same great
### Whats included
- Prioritised Support tickets via support@stirlingpdf.com to reach directly to Stirling-PDF team for support and 1:1 meetings where applicable (Provided they come from same email domain registered with us)
- Prioritised Enhancements to Stirling-PDF where applicable
- Prioritised Enhancements to Stirling-PDF where applicable
- Base SSO support
- Advanced SSO such as automated login handling (Coming very soon)
- SAML SSO (Coming very soon)
@@ -404,7 +405,7 @@ To access your account settings, go to Account Settings in the settings cog menu
To add new users, go to the bottom of Account Settings and hit 'Admin Settings'. Here you can add new users. The different roles mentioned within this are for rate limiting. This is a work in progress and will be expanded on more in the future.
For API usage, you must provide a header with `X-API-Key` and the associated API key for that user.
For API usage, you must provide a header with `X-API-KEY` and the associated API key for that user.
## FAQ

View File

@@ -8,6 +8,7 @@ plugins {
id "com.diffplug.spotless" version "6.25.0"
id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.0"
}
@@ -22,11 +23,11 @@ ext {
lombokVersion = "1.18.36"
bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
openSamlVersion = "4.3.2"
}
group = "stirling.software"
version = "0.36.0"
version = "0.36.3"
java {
@@ -41,6 +42,9 @@ repositories {
maven {
url 'https://build.shibboleth.net/maven/releases'
}
maven { url "https://build.shibboleth.net/maven/releases" }
maven { url "https://maven.pkg.github.com/jcefmaven/jcefmaven" }
}
licenseReport {
@@ -64,6 +68,12 @@ sourceSets {
exclude "stirling/software/SPDF/model/User.java"
exclude "stirling/software/SPDF/repository/**"
}
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
exclude "stirling/software/SPDF/UI/impl/**"
}
}
}
}
@@ -74,16 +84,153 @@ openApi {
outputFileName = "SwaggerDoc.json"
}
//0.11.5 to 2024.11.5
def getMacVersion(String version) {
def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
}
jpackage {
input = "build/libs"
appName = "Stirling-PDF"
appVersion = project.version
vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor"
mainJar = "Stirling-PDF-${project.version}.jar"
mainClass = "org.springframework.boot.loader.launch.JarLauncher"
icon = "src/main/resources/static/favicon.ico"
// JVM Options
javaOptions = [
"-DBROWSER_OPEN=true",
"-DSTIRLING_PDF_DESKTOP_UI=true",
"-Djava.awt.headless=false",
"-Dapple.awt.UIElement=true",
"--add-opens", "java.base/java.lang=ALL-UNNAMED",
"--add-opens", "java.desktop/java.awt.event=ALL-UNNAMED",
"--add-opens", "java.desktop/sun.awt=ALL-UNNAMED"
]
verbose = true
destination = "${projectDir}/build/jpackage"
// Windows-specific configuration
windows {
launcherAsService = false
appVersion = project.version
winConsole = false
winDirChooser = true
winMenu = true
winShortcut = true
winPerUserInstall = true
winMenuGroup = "Stirling Software"
winUpgradeUuid = "2a43ed0c-b8c2-40cf-89e1-751129b87641" // Unique identifier for updates
winHelpUrl = "https://github.com/Stirling-Tools/Stirling-PDF"
winUpdateUrl = "https://github.com/Stirling-Tools/Stirling-PDF/releases"
type = "exe"
installDir = "C:/Program Files/Stirling-PDF"
}
// macOS-specific configuration
mac {
appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns"
type = "dmg"
macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially
//installDir = "Applications"
// Add license and other documentation to DMG
/*macDmgContent = [
"README.md",
"LICENSE",
"CHANGELOG.md"
]*/
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
}
// Linux-specific configuration
linux {
appVersion = project.version
icon = "src/main/resources/static/favicon.png"
type = "deb" // Can also use "rpm" for Red Hat-based systems
// Debian package configuration
//linuxPackageName = "stirlingpdf"
linuxDebMaintainer = "support@stirlingpdf.com"
linuxMenuGroup = "Office;PDF;Productivity"
linuxAppCategory = "Office"
linuxAppRelease = "1"
linuxPackageDeps = true
installDir = "/opt/Stirling-PDF"
// RPM-specific settings
//linuxRpmLicenseType = "MIT"
}
// Common additional options
//jLinkOptions = [
// "--strip-debug",
// "--compress=2",
// "--no-header-files",
// "--no-man-pages"
//]
// Add any additional modules required
/*addModules = [
"java.base",
"java.desktop",
"java.logging",
"java.sql",
"java.xml",
"jdk.crypto.ec"
]*/
// Add copyright and license information
copyright = "Copyright © 2024 Stirling Software"
licenseFile = "LICENSE"
}
launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico"
outfile="Stirling-PDF.exe"
headerType="console"
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
headerType = "gui"
} else {
headerType = "console"
}
jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"]
if(System.getenv("STIRLING_PDF_DESKTOP_UI") == 'true') {
variables=["BROWSER_OPEN=true", "STIRLING_PDF_DESKTOP_UI=true"]
} else {
variables=["BROWSER_OPEN=true"]
}
jreMinVersion="17"
mutexName="Stirling-PDF"
@@ -123,6 +270,13 @@ configurations.all {
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
dependencies {
if (System.getenv("STIRLING_PDF_DESKTOP_UI") != "false") {
implementation "me.friwi:jcefmaven:127.3.1"
implementation "org.openjfx:javafx-controls:21"
implementation "org.openjfx:javafx-swing:21"
}
//security updates
implementation "org.springframework:spring-webmvc:6.2.0"
@@ -142,12 +296,12 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232"
@@ -271,7 +425,14 @@ jar {
tasks.named("test") {
useJUnitPlatform()
}
task printVersion {
println project.version
doLast {
println project.version
}
}
task printMacVersion {
doLast {
println getMacVersion(project.version.toString())
}
}

View File

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

View File

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

View File

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

View File

@@ -28,7 +28,7 @@ public class LicenseKeyChecker {
this.checkLicense();
}
@Scheduled(fixedRate = 604800000) // 7 days in milliseconds
@Scheduled(initialDelay = 604800000, fixedRate = 604800000) // 7 days in milliseconds
public void checkLicensePeriodically() {
checkLicense();
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -260,6 +260,9 @@ public class EndpointConfiguration {
// Pdftohtml dependent endpoints
addEndpointToGroup("Pdftohtml", "pdf-to-html");
// disabled for now while we resolve issues
disableEndpoint("pdf-to-pdfa");
}
private void processEnvironmentConfigs() {

View File

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

View File

@@ -75,5 +75,6 @@ public class InitialSecuritySetup {
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId());
}
userService.syncCustomApiUser(applicationProperties.getSecurity().getCustomGlobalAPIKey());
}
}

View File

@@ -99,7 +99,7 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
if (applicationProperties.getSecurity().getCsrfDisabled() || !loginEnabledValue) {
http.csrf(csrf -> csrf.disable());
}
@@ -116,7 +116,7 @@ public class SecurityConfiguration {
csrf ->
csrf.ignoringRequestMatchers(
request -> {
String apiKey = request.getHeader("X-API-Key");
String apiKey = request.getHeader("X-API-KEY");
// If there's no API key, don't ignore CSRF
// (return false)
@@ -289,17 +289,17 @@ public class SecurityConfiguration {
}
} else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
new CsrfTokenRequestAttributeHandler();
requestHandler.setCsrfRequestAttributeName(null);
http.csrf(
csrf ->
csrf.csrfTokenRepository(cookieRepo)
.csrfTokenRequestHandler(requestHandler));
}
// if (!applicationProperties.getSecurity().getCsrfDisabled()) {
// CookieCsrfTokenRepository cookieRepo =
// CookieCsrfTokenRepository.withHttpOnlyFalse();
// CsrfTokenRequestAttributeHandler requestHandler =
// new CsrfTokenRequestAttributeHandler();
// requestHandler.setCsrfRequestAttributeName(null);
// http.csrf(
// csrf ->
// csrf.csrfTokenRepository(cookieRepo)
// .csrfTokenRequestHandler(requestHandler));
// }
http.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}

View File

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

View File

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

View File

@@ -390,6 +390,37 @@ public class UserService implements UserServiceInterface {
}
}
@Transactional
public void syncCustomApiUser(String customApiKey) throws IOException {
if (customApiKey == null || customApiKey.trim().length() == 0) {
return;
}
String username = "CUSTOM_API_USER";
Optional<User> existingUser = findByUsernameIgnoreCase(username);
if (!existingUser.isPresent()) {
// Create new user with API role
User user = new User();
user.setUsername(username);
user.setPassword(UUID.randomUUID().toString());
user.setEnabled(true);
user.setFirstLogin(false);
user.setAuthenticationType(AuthenticationType.WEB);
user.setApiKey(customApiKey);
user.addAuthority(new Authority(Role.INTERNAL_API_USER.getRoleId(), user));
userRepository.save(user);
databaseBackupHelper.exportDatabase();
} else {
// Update API key if it has changed
User user = existingUser.get();
if (!customApiKey.equals(user.getApiKey())) {
user.setApiKey(customApiKey);
userRepository.save(user);
databaseBackupHelper.exportDatabase();
}
}
}
@Override
public long getTotalUsersCount() {
return userRepository.count();

View File

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

View File

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

View File

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

View File

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

View File

@@ -87,7 +87,7 @@ public class OCRController {
Files.createDirectories(tempOutputDir);
Files.createDirectories(tempImagesDir);
Process process = null;
try {
// Save input file
inputFile.transferTo(tempInputFile.toFile());
@@ -139,7 +139,7 @@ public class OCRController {
command.add("pdf"); // Always output PDF
ProcessBuilder pb = new ProcessBuilder(command);
Process process = pb.start();
process = pb.start();
// Capture any error output
try (BufferedReader reader =
@@ -188,6 +188,10 @@ public class OCRController {
.body(pdfContent);
} finally {
if (process != null) {
process.destroy();
}
// Clean up temporary files
deleteDirectory(tempDir);
}

View File

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

View File

@@ -595,7 +595,9 @@ public class GetInfoOnPDF {
permissionsNode.put("Document Assembly", getPermissionState(ap.canAssembleDocument()));
permissionsNode.put("Extracting Content", getPermissionState(ap.canExtractContent()));
permissionsNode.put("Extracting for accessibility", getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put(
"Extracting for accessibility",
getPermissionState(ap.canExtractForAccessibility()));
permissionsNode.put("Form Filling", getPermissionState(ap.canFillInForm()));
permissionsNode.put("Modifying", getPermissionState(ap.canModify()));
permissionsNode.put("Modifying annotations", getPermissionState(ap.canModifyAnnotations()));

View File

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

View File

@@ -92,20 +92,29 @@ public class ValidateSignatureController {
SignerInformationStore signerStore = signedData.getSignerInfos();
for (SignerInformation signer : signerStore.getSigners()) {
X509CertificateHolder certHolder = (X509CertificateHolder) certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert = new JcaX509CertificateConverter().getCertificate(certHolder);
X509CertificateHolder certHolder =
(X509CertificateHolder)
certStore.getMatches(signer.getSID()).iterator().next();
X509Certificate cert =
new JcaX509CertificateConverter().getCertificate(certHolder);
boolean isValid = signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
boolean isValid =
signer.verify(new JcaSimpleSignerInfoVerifierBuilder().build(cert));
result.setValid(isValid);
// Additional validations
result.setChainValid(customCert != null
? certValidationService.validateCertificateChainWithCustomCert(cert, customCert)
: certValidationService.validateCertificateChain(cert));
result.setChainValid(
customCert != null
? certValidationService
.validateCertificateChainWithCustomCert(
cert, customCert)
: certValidationService.validateCertificateChain(cert));
result.setTrustValid(customCert != null
? certValidationService.validateTrustWithCustomCert(cert, customCert)
: certValidationService.validateTrustStore(cert));
result.setTrustValid(
customCert != null
? certValidationService.validateTrustWithCustomCert(
cert, customCert)
: certValidationService.validateTrustStore(cert));
result.setNotRevoked(!certValidationService.isRevoked(cert));
result.setNotExpired(!cert.getNotAfter().before(new Date()));
@@ -123,17 +132,18 @@ public class ValidateSignatureController {
result.setValidFrom(cert.getNotBefore().toString());
result.setValidUntil(cert.getNotAfter().toString());
result.setSignatureAlgorithm(cert.getSigAlgName());
// Get key size (if possible)
try {
result.setKeySize(((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
result.setKeySize(
((RSAPublicKey) cert.getPublicKey()).getModulus().bitLength());
} catch (Exception e) {
// If not RSA or error, set to 0
result.setKeySize(0);
}
result.setVersion(String.valueOf(cert.getVersion()));
// Set key usage
List<String> keyUsages = new ArrayList<>();
boolean[] keyUsageFlags = cert.getKeyUsage();
@@ -150,9 +160,11 @@ public class ValidateSignatureController {
}
}
result.setKeyUsages(keyUsages);
// Check if self-signed
result.setSelfSigned(cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal()));
result.setSelfSigned(
cert.getSubjectX500Principal()
.equals(cert.getIssuerX500Principal()));
}
} catch (Exception e) {
result.setValid(false);

View File

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

View File

@@ -73,6 +73,7 @@ public class ApplicationProperties {
private int loginAttemptCount;
private long loginResetTimeMinutes;
private String loginMethod = "all";
private String customGlobalAPIKey;
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
@@ -285,6 +286,7 @@ public class ApplicationProperties {
public static class AutomaticallyGenerated {
@ToString.Exclude private String key;
private String UUID;
private String appVersion;
}
@Data

View File

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

View File

@@ -16,16 +16,15 @@ public class SignatureValidationResult {
private boolean trustValid;
private boolean notExpired;
private boolean notRevoked;
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm;// Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
private String issuerDN; // Certificate issuer's Distinguished Name
private String subjectDN; // Certificate subject's Distinguished Name
private String serialNumber; // Certificate serial number
private String validFrom; // Certificate validity start date
private String validUntil; // Certificate validity end date
private String signatureAlgorithm; // Algorithm used for signing
private int keySize; // Key size in bits
private String version; // Certificate version
private List<String> keyUsages; // List of key usage purposes
private boolean isSelfSigned; // Whether the certificate is self-signed
}

View File

@@ -1,6 +1,5 @@
package stirling.software.SPDF.service;
import io.github.pixee.security.BoundedLineReader;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -24,6 +23,8 @@ import java.util.Set;
import org.springframework.stereotype.Service;
import io.github.pixee.security.BoundedLineReader;
import jakarta.annotation.PostConstruct;
@Service

View File

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

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=الصفحات المحددة
multiTool.undo=تراجع
multiTool.redo=إعادة إجراء
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
@@ -1056,6 +1066,7 @@ addPassword.submit=تشفير
#watermark
watermark.title=إضافة علامة مائية
watermark.header=إضافة علامة مائية
watermark.customColor=لون نص مخصص
watermark.selectText.1=حدد PDF لإضافة العلامة المائية إليه:
watermark.selectText.2=نص العلامة المائية:
watermark.selectText.3=حجم الخط:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Seçilmiş Səhifə(lər)
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=Bu xüsusiyyət bizim <a href="{0}">multi-alət səhifə</a>mizdə də mövcuddur. Əlavə xüsusiyyətlər və səhifə-səhifə interfeys üçün sınaqdan keçirin!
@@ -1056,6 +1066,7 @@ addPassword.submit=Şifrlə
#watermark
watermark.title=Watermark Əlavə Et
watermark.header=Watermark Əlavə Et
watermark.customColor=Fərdi Mətn Rəngi
watermark.selectText.1=Watermark əlavə olunacaq PDF-i seç
watermark.selectText.2=Watermark Mətni:
watermark.selectText.3=Şrift Ölçüsü:
@@ -1280,7 +1291,6 @@ releases.header=Buraxılış Qeydləri
releases.current.version=Hazırki Buraxılış
releases.note=Buraxılış Qeydləri yalnız ingiliscə mövcuddur
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Шифроване
#watermark
watermark.title=Добавяне на воден знак
watermark.header=Добавяне на воден знак
watermark.customColor=Персонализиран цвят на текста
watermark.selectText.1=Изберете PDF, към който да добавите воден знак:
watermark.selectText.2=Текст на воден знак:
watermark.selectText.3=Размер на шрифта:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Encripta
#watermark
watermark.title=Afegir Marca d'Aigua
watermark.header=Afegir Marca d'Aigua
watermark.customColor=Color de Text Personalitzat
watermark.selectText.1=Selecciona el PDF per afegir la Marca d'Aigua:
watermark.selectText.2=Text de la Marca d'Aigua
watermark.selectText.3=Mida de la Font:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Šifrovat
#watermark
watermark.title=Přidat vodoznak
watermark.header=Přidat vodoznak
watermark.customColor=Vlastní barva textu
watermark.selectText.1=Vyberte PDF, ke kterému chcete přidat vodoznak:
watermark.selectText.2=Text vodoznaku:
watermark.selectText.3=Velikost písma:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Kryptér
#watermark
watermark.title=Tilføj Vandmærke
watermark.header=Tilføj Vandmærke
watermark.customColor=Brugerdefineret Tekstfarve
watermark.selectText.1=Vælg PDF til at tilføje vandmærke:
watermark.selectText.2=Vandmærketekst:
watermark.selectText.3=Skriftstørrelse:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -146,8 +146,8 @@ navbar.search=Suche
navbar.sections.organize=Organisieren
navbar.sections.convertTo=In PDF konvertieren
navbar.sections.convertFrom=Konvertieren von PDF
navbar.sections.security=Zeichen und Sicherheit
navbar.sections.advance=Fortschrittlich
navbar.sections.security=Signieren und Sicherheit
navbar.sections.advance=Erweiterte Funktionen
navbar.sections.edit=Anzeigen und Bearbeiten
navbar.sections.popular=Beliebt
@@ -248,7 +248,7 @@ database.fileNullOrEmpty=Datei darf nicht null oder leer sein
database.failedImportFile=Dateiimport fehlgeschlagen
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
session.refreshPage=Refresh Page
session.refreshPage=Seite aktualisieren
#############
# HOME-PAGE #
@@ -512,9 +512,9 @@ home.splitPdfByChapters.title=PDF-Datei nach Kapiteln aufteilen
home.splitPdfByChapters.desc=Aufteilung einer PDF-Datei in mehrere Dateien auf Basis der Kapitelstruktur.
splitPdfByChapters.tags=aufteilen,kapitel,lesezeichen,organisieren
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
home.validateSignature.title=PDF-Signatur überprüfen
home.validateSignature.desc=Digitale Signaturen und Zertifikate in PDF-Dokumenten überprüfen
validateSignature.tags=signature,verify,validate,pdf,digitale signatur,signatur validieren,überprüfen,Zertifikat,cert
#replace-invert-color
replace-color.title=Farbe Ersetzen-Invertieren
@@ -822,12 +822,12 @@ sign.save=Signature speichern
sign.personalSigs=Persönliche Signaturen
sign.sharedSigs=Geteilte Signaturen
sign.noSavedSigs=Es wurden keine gespeicherten Signaturen gefunden
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
sign.addToAll=Zu allen Seiten hinzufügen
sign.delete=Löschen
sign.first=Erste Seite
sign.last=Letzte Seite
sign.next=Nächste Seite
sign.previous=Vorherige Seite
#repair
repair.title=Reparieren
@@ -953,17 +953,27 @@ multiTool.deleteSelected=Auswahl löschen
multiTool.downloadAll=Downloaden
multiTool.downloadSelected=Auswahl downloaden
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
multiTool.insertPageBreak=Seitenumbruch einfügen
multiTool.addFile=Datei hinzufügen
multiTool.rotateLeft=Nach links drehen
multiTool.rotateRight=Nach rechts drehen
multiTool.split=Teilen
multiTool.moveLeft=Nach links verschieben
multiTool.moveRight=Nach rechts verschieben
multiTool.delete=Löschen
multiTool.dragDropMessage=Ausgewählte Seite(n)
multiTool.undo=Rückgängig machen
multiTool.redo=Wiederherstellen
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen!
@@ -1056,6 +1066,7 @@ addPassword.submit=Verschlüsseln
#watermark
watermark.title=Wasserzeichen hinzufügen
watermark.header=Wasserzeichen hinzufügen
watermark.customColor=Benutzerdefinierte Textfarbe
watermark.selectText.1=PDF auswählen, dem ein Wasserzeichen hinzugefügt werden soll:
watermark.selectText.2=Wasserzeichen Text:
watermark.selectText.3=Schriftgröße:
@@ -1268,50 +1279,49 @@ splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, kön
splitByChapters.submit=PDF teilen
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.click=Klicken
fileChooser.or=oder
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
fileChooser.hoveredDragAndDrop=Datei(en) hierhin Ziehen & Fallenlassen
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
releases.footer=Veröffentlichungen
releases.title=Versionshinweise
releases.header=Versionshinweise
releases.current.version=Aktuelle Version
releases.note=Versionshinweise sind nur auf Englisch verfügbar
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.title=PDF-Signaturen überprüfen
validateSignature.header=Digitale Signaturen überprüfen
validateSignature.selectPDF=Signierte PDF-Datei auswählen
validateSignature.submit=Signaturen überprüfen
validateSignature.results=Gültigkeitsprüfungsergebnisse
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.signer=Unterzeichner
validateSignature.date=Datum
validateSignature.reason=Grund
validateSignature.location=Ort
validateSignature.noSignatures=Keine digitalen Signaturen in diesem Dokument gefunden
validateSignature.status.valid=Gültig
validateSignature.status.invalid=Ungültig
validateSignature.chain.invalid=Zertifikatskettenprüfung fehlgeschlagen - kann die Identität des Unterzeichners nicht verifizieren
validateSignature.trust.invalid=Zertifikat nicht im Truststore - Quelle kann nicht verifiziert werden
validateSignature.cert.expired=Zertifikat ist abgelaufen
validateSignature.cert.revoked=Zertifikat wurde widerrufen
validateSignature.signature.info=Signaturinformationen
validateSignature.signature=Signatur
validateSignature.signature.mathValid=Signatur ist mathematisch gültig ABER:
validateSignature.selectCustomCert=Benutzerdefinierte Zertifikatsdatei X.509 (Optional)
validateSignature.cert.info=Zertifikat Details
validateSignature.cert.issuer=Aussteller
validateSignature.cert.subject=Betreff
validateSignature.cert.serialNumber=Seriennummer
validateSignature.cert.validFrom=Gültig von
validateSignature.cert.validUntil=Gültig bis
validateSignature.cert.algorithm=Algorithmus
validateSignature.cert.keySize=Schlüsselgröße
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.keyUsage=Schlüsselverwendung
validateSignature.cert.selfSigned=Selbstsigniert
validateSignature.cert.bits=bits

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Κρυπτογράφηση
#watermark
watermark.title=Προσθήκη Υδατογραφήματος
watermark.header=Προσθήκη Υδατογραφήματος
watermark.customColor=Προσαρμοσμένο χρώμα κειμένου
watermark.selectText.1=Επιλέξτε PDF για την προσθήκη του υδατογραφήματος:
watermark.selectText.2=Κείμενο Υδατογραφήματος:
watermark.selectText.3=Μέγεθος Κειμένου:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -73,7 +73,7 @@ joinDiscord=Join our Discord server
seeDockerHub=See Docker Hub
visitGithub=Visit Github Repository
donate=Donate
color=Color
color=Colour
sponsor=Sponsor
info=Info
pro=Pro
@@ -419,9 +419,9 @@ home.auto-rename.title=Auto Rename PDF File
home.auto-rename.desc=Auto renames a PDF file based on its detected header
auto-rename.tags=auto-detect,header-based,organize,relabel
home.adjust-contrast.title=Adjust Colors/Contrast
home.adjust-contrast.title=Adjust Colours/Contrast
home.adjust-contrast.desc=Adjust Contrast, Saturation and Brightness of a PDF
adjust-contrast.tags=color-correction,tune,modify,enhance
adjust-contrast.tags=color-correction,tune,modify,enhance,colour-correction
home.crop.title=Crop PDF
home.crop.desc=Crop a PDF to reduce its size (maintains text!)
@@ -488,11 +488,11 @@ overlay-pdfs.tags=Overlay
home.split-by-sections.title=Split PDF by Sections
home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections
split-by-sections.tags=Section Split, Divide, Customize
split-by-sections.tags=Section Split, Divide, Customize,Customise
home.AddStampRequest.title=Add Stamp to PDF
home.AddStampRequest.desc=Add text or add image stamps at set locations
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize,Customise
home.PDFToBook.title=PDF to Book
@@ -518,21 +518,21 @@ validateSignature.tags=signature,verify,validate,pdf,certificate,digital signatu
#replace-invert-color
replace-color.title=Advanced Colour options
replace-color.header=Replace-Invert Color PDF
replace-color.header=Replace-Invert Colour PDF
home.replaceColorPdf.title=Advanced Colour options
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
home.replaceColorPdf.desc=Replace colour for text and background in PDF and invert full colour of pdf to reduce file size
replaceColorPdf.tags=Replace Colour,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert colour Options
replace-color.selectText.2=Default(Default high contrast colours)
replace-color.selectText.3=Custom(Customised colours)
replace-color.selectText.4=Full-Invert(Invert all colours)
replace-color.selectText.5=High contrast colour options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.selectText.10=Choose text Colour
replace-color.selectText.11=Choose background Colour
replace-color.submit=Replace
@@ -655,7 +655,7 @@ AddStampRequest.position=Position
AddStampRequest.overrideX=Override X Coordinate
AddStampRequest.overrideY=Override Y Coordinate
AddStampRequest.customMargin=Custom Margin
AddStampRequest.customColor=Custom Text Color
AddStampRequest.customColor=Custom Text Colour
AddStampRequest.submit=Submit
@@ -787,8 +787,8 @@ removeAnnotations.submit=Remove
#compare
compare.title=Compare
compare.header=Compare PDFs
compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Color 2:
compare.highlightColor.1=Highlight Colour 1:
compare.highlightColor.2=Highlight Colour 2:
compare.document.1=Document 1
compare.document.2=Document 2
compare.submit=Compare
@@ -846,7 +846,7 @@ flatten.submit=Flatten
ScannerImageSplit.selectText.1=Angle Threshold:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
ScannerImageSplit.selectText.3=Tolerance:
ScannerImageSplit.selectText.4=Determines the range of color variation around the estimated background color (default: 30).
ScannerImageSplit.selectText.4=Determines the range of colour variation around the estimated background colour (default: 30).
ScannerImageSplit.selectText.5=Minimum Area:
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000).
ScannerImageSplit.selectText.7=Minimum Contour Area:
@@ -898,7 +898,7 @@ compress.title=Compress
compress.header=Compress PDF
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
compress.selectText.1=Manual Mode - From 1 to 4
compress.selectText.2=Optimization level:
compress.selectText.2=Optimisation level:
compress.selectText.3=4 (Terrible for text images)
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Encrypt
#watermark
watermark.title=Add Watermark
watermark.header=Add Watermark
watermark.customColor=Custom Text Colour
watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Encrypt
#watermark
watermark.title=Add Watermark
watermark.header=Add Watermark
watermark.customColor=Custom Text Color
watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Encriptar
#watermark
watermark.title=Añadir marca de agua
watermark.header=Añadir marca de agua
watermark.customColor=Personalizar color de texto
watermark.selectText.1=Seleccionar PDF para añadir marca de agua:
watermark.selectText.2=Texto de la marca de agua:
watermark.selectText.3=Tamaño de la Fuente:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Enkriptatu
#watermark
watermark.title=Gehitu ur-marka
watermark.header=Gehitu ur-marka
watermark.customColor=Custom Text Color
watermark.selectText.1=Hautatu PDFa ur-marka gehitzeko:
watermark.selectText.2=Ur-markaren testua:
watermark.selectText.3=Letra-tipoaren tamaina:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

File diff suppressed because it is too large Load Diff

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) sélectionnées
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=Cette fonctionnalité est aussi disponible dans la <a href="{0}">page de l'outil multifonction</a>. Allez-y pour une interface page par page améliorée et des fonctionnalités additionnelles !
@@ -1056,6 +1066,7 @@ addPassword.submit=Chiffrer
#watermark
watermark.title=Ajouter un filigrane
watermark.header=Ajouter un filigrane
watermark.customColor=Couleur de texte personnalisée
watermark.selectText.1=PDF auquel ajouter un filigrane
watermark.selectText.2=Texte du filigrane
watermark.selectText.3=Taille de police
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Criptigh
#watermark
watermark.title=Cuir Uisce leis
watermark.header=Cuir Uisce leis
watermark.customColor=Dath Téacs Saincheaptha
watermark.selectText.1=Roghnaigh PDF chun comhartha uisce a chur leis:
watermark.selectText.2=Téacs Comhartha Uisce:
watermark.selectText.3=Méid cló:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=एन्क्रिप्ट करें
#watermark
watermark.title=वॉटरमार्क जोड़ें
watermark.header=वॉटरमार्क जोड़ें
watermark.customColor=संवैधित टेक्स्ट रंग
watermark.selectText.1=वॉटरमार्क जोड़ने के लिए पीडीएफ चुनें:
watermark.selectText.2=वॉटरमार्क टेक्स्ट:
watermark.selectText.3=फ़ॉन्ट साइज़:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Šifriraj
#watermark
watermark.title=Dodaj vodeni žig
watermark.header=Dodaj vodeni žig
watermark.customColor=Prilagođena boja teksta
watermark.selectText.1=Izaberite PDF za dodavanje vodenog žiga:
watermark.selectText.2=Tekst vodenog žiga:
watermark.selectText.3=Veličina fonta:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Titkosítás
#watermark
watermark.title=Vízjel hozzáadása
watermark.header=Vízjel hozzáadása
watermark.customColor=Egyéni szövegszín
watermark.selectText.1=Válassza ki a PDF-t, amelyhez vízjelet kíván hozzáadni:
watermark.selectText.2=Vízjel szövege:
watermark.selectText.3=Betűméret:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Enkripsi
#watermark
watermark.title=Tambahkan Watermark
watermark.header=Tambahkan Watermark
watermark.customColor=Warna Teks Kustom
watermark.selectText.1=Pilih PDF untuk menambahkan watermark:
watermark.selectText.2=Text Watermark:
watermark.selectText.3=Ukuran Huruf:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Pagina(e) selezionata(e)
multiTool.undo=Annulla
multiTool.redo=Rifai
#decrypt
decrypt.passwordPrompt=Questo file è protetto da password. Inserisci la password:
decrypt.cancelled=Operazione annullata per il PDF: {0}
decrypt.noPassword=Nessuna password fornita per il PDF crittografato: {0}
decrypt.invalidPassword=Riprova con la password corretta.
decrypt.invalidPasswordHeader=Password errata o crittografia non supportata per il PDF: {0}
decrypt.unexpectedError=Si è verificato un errore durante l'elaborazione del file. Riprova..
decrypt.serverError=Errore del server durante la decrittazione: {0}
decrypt.success=File decrittografato con successo.
#multiTool-advert
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
@@ -1056,6 +1066,7 @@ addPassword.submit=Crittografa
#watermark
watermark.title=Aggiungi Filigrana
watermark.header=Aggiungi filigrana
watermark.customColor=Colore testo personalizzato
watermark.selectText.1=Seleziona PDF a cui aggiungere la filigrana:
watermark.selectText.2=Testo:
watermark.selectText.3=Dimensione carattere:
@@ -1280,7 +1291,6 @@ releases.header=Note di rilascio
releases.current.version=Rilascio corrente
releases.note=Le note di rilascio sono disponibili solo in inglese
#Validate Signature
#Validate Signature
validateSignature.title=Validare le firme PDF
validateSignature.header=Convalidare le firme digitali

View File

@@ -56,12 +56,12 @@ userNotFoundMessage=ユーザーが見つかりません。
incorrectPasswordMessage=現在のパスワードが正しくありません。
usernameExistsMessage=新しいユーザー名はすでに存在します。
invalidUsernameMessage=ユーザー名が無効です。ユーザー名には文字、数字、およびそれに続く特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
invalidPasswordMessage=パスワードは空にすることはできません。また、先頭・末尾にスペースを含めることもできません。
confirmPasswordErrorMessage=新しいパスワードと新しいパスワードの確認は一致する必要があります。
deleteCurrentUserMessage=現在ログインしているユーザーは削除できません。
deleteUsernameExistsMessage=そのユーザー名は存在しないため削除できません。
downgradeCurrentUserMessage=現在のユーザーの役割をダウングレードできません
disabledCurrentUserMessage=The current user cannot be disabled
disabledCurrentUserMessage=現在のユーザーを無効にすることはできません
downgradeCurrentUserLongMessage=現在のユーザーの役割をダウングレードできません。したがって、現在のユーザーは表示されません。
userAlreadyExistsOAuthMessage=ユーザーは既にOAuth2ユーザーとして存在します。
userAlreadyExistsWebMessage=ユーザーは既にWebユーザーとして存在します。
@@ -76,12 +76,12 @@ donate=寄付する
color=
sponsor=スポンサー
info=Info
pro=Pro
page=Page
pages=Pages
loading=Loading...
addToDoc=Add to Document
reset=Reset
pro=pro
page=ページ
pages=ページ
loading=読込中...
addToDoc=ドキュメントに追加
reset=リセット
legal.privacy=プライバシーポリシー
legal.terms=利用規約
@@ -92,7 +92,7 @@ legal.impressum=著作権利者情報
###############
# Pipeline #
###############
pipeline.header=パイプラインメニュー (Alpha)
pipeline.header=パイプラインメニュー (Beta)
pipeline.uploadButton=カスタムのアップロード
pipeline.configureButton=設定
pipeline.defaultOption=カスタム
@@ -117,21 +117,21 @@ pipelineOptions.validateButton=検証
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
enterpriseEdition.button=Proにアップグレード
enterpriseEdition.warning=この機能はProユーザーのみが利用できます。
enterpriseEdition.yamlAdvert=Stirling PDF Proは、YAML構成ファイルやその他のSSO機能をサポートしています。
enterpriseEdition.ssoAdvert=より多くのユーザー管理機能をお探しですか? Stirling PDF Proをご覧ください
#################
# Analytics #
#################
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
analytics.title=Stirling PDFをもっと良くしたいですか
analytics.paragraph1=Stirling PDFでは、製品の改善に役立つ分析機能をオプトインしています。個人情報やファイルの内容を追跡することはありません。
analytics.paragraph2=Stirling-PDFの成長を支援しユーザーをより深く理解できるように分析を有効にすることを検討してください。
analytics.enable=分析を有効にする
analytics.disable=分析を無効にする
analytics.settings=config/settings.ymlファイルでアナリティクスの設定を変更できます。
#############
# NAVBAR #
@@ -142,14 +142,14 @@ navbar.language=言語
navbar.settings=設定
navbar.allTools=ツール
navbar.multiTool=マルチツール
navbar.search=Search
navbar.search=検索
navbar.sections.organize=整理
navbar.sections.convertTo=PDFへ変換
navbar.sections.convertFrom=PDFから変換
navbar.sections.security=署名とセキュリティ
navbar.sections.advance=アドバンスド
navbar.sections.edit=閲覧と編集
navbar.sections.popular=Popular
navbar.sections.popular=人気
#############
# SETTINGS #
@@ -208,7 +208,7 @@ adminUserSettings.user=ユーザー
adminUserSettings.addUser=新しいユーザを追加
adminUserSettings.deleteUser=ユーザの削除
adminUserSettings.confirmDeleteUser=ユーザを本当に削除しますか?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.confirmChangeUserStatus=ユーザーを無効/有効にする必要がありますか?
adminUserSettings.usernameInfo=ユーザー名には、文字、数字、および次の特殊文字 @._+- のみを含めることができます。または、有効な電子メール アドレスである必要があります。
adminUserSettings.roles=役割
adminUserSettings.role=役割
@@ -247,8 +247,8 @@ database.fileNotFound=ファイルが見つかりません
database.fileNullOrEmpty=ファイルは null または空であってはなりません
database.failedImportFile=ファイルのインポートに失敗
session.expired=Your session has expired. Please refresh the page and try again.
session.refreshPage=Refresh Page
session.expired=セッションが期限切れです。ページを更新してもう一度お試しください。
session.refreshPage=ページを更新
#############
# HOME-PAGE #
@@ -488,52 +488,52 @@ overlay-pdfs.tags=Overlay
home.split-by-sections.title=PDFをセクションで分割
home.split-by-sections.desc=PDFの各ページを縦横に分割します。
split-by-sections.tags=Section Split, Divide, Customize
split-by-sections.tags=Section Split, Divide, Customize,Customise
home.AddStampRequest.title=PDFにスタンプを追加
home.AddStampRequest.desc=設定した位置にテキストや画像のスタンプを追加できます
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize,Customise
home.PDFToBook.title=PDFを書籍に変換
home.PDFToBook.desc=calibreを使用してPDFを書籍/コミック形式に変換します
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.BookToPDF.title=PDFを書籍に変換
home.BookToPDF.desc=calibreを使用してPDFを書籍/コミック形式に変換します
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.removeImagePdf.title=画像の削除
home.removeImagePdf.desc=PDFから画像を削除してファイルサイズを小さくします
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
home.splitPdfByChapters.title=PDFをチャプターごとに分割
home.splitPdfByChapters.desc=チャプターの構造に基づいてPDFを複数のファイルに分割します
splitPdfByChapters.tags=split,chapters,bookmarks,organize
home.validateSignature.title=Validate PDF Signature
home.validateSignature.desc=Verify digital signatures and certificates in PDF documents
home.validateSignature.title=PDF署名の検証
home.validateSignature.desc=PDF文書のデジタル署名と証明書を検証します
validateSignature.tags=signature,verify,validate,pdf,certificate,digital signature,Validate Signature,Validate certificate
#replace-invert-color
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replace-color.title=色の置換・反転
replace-color.header=PDFの色の置換・反転
home.replaceColorPdf.title=色の置換と反転
home.replaceColorPdf.desc=PDF内のテキストと背景の色を置き換え、PDFのフルカラーを反転してファイルサイズを縮小します。
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
replace-color.selectText.1=色の置換または反転オプション
replace-color.selectText.2=デフォルト(デフォルトの高コントラスト色)
replace-color.selectText.3=カスタム(カスタマイズされた色)
replace-color.selectText.4=フル反転(すべての色を反転)
replace-color.selectText.5=高コントラストカラーオプション
replace-color.selectText.6=黒背景に白文字
replace-color.selectText.7=白背景に黒文字
replace-color.selectText.8=黒背景に黄色文字
replace-color.selectText.9=黒背景に緑文字
replace-color.selectText.10=テキストの色を選択
replace-color.selectText.11=背景色を選択
replace-color.submit=置換
@@ -560,9 +560,9 @@ login.oauth2AccessDenied=アクセス拒否
login.oauth2InvalidTokenResponse=無効なトークン応答
login.oauth2InvalidIdToken=無効なIDトークン
login.userIsDisabled=ユーザーは非アクティブ化されており、現在このユーザー名でのログインはブロックされています。管理者に連絡してください。
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.alreadyLoggedIn=すでにログインしています
login.alreadyLoggedIn2=デバイスからログアウトしてもう一度お試しください。
login.toManySessions=アクティブなセッションが多すぎます
#auto-redact
autoRedact.title=自動塗りつぶし
@@ -578,8 +578,8 @@ autoRedact.submitButton=送信
#showJS
showJS.title=JavaScriptを表示
showJS.header=JavaScriptを表示
showJS.title=Javascriptを表示
showJS.header=Javascriptを表示
showJS.downloadJS=Javascriptをダウンロード
showJS.submit=表示
@@ -757,7 +757,7 @@ certSign.showSig=署名を表示
certSign.reason=理由
certSign.location=場所
certSign.name=名前
certSign.showLogo=Show Logo
certSign.showLogo=ロゴを表示
certSign.submit=PDFに署名
@@ -792,9 +792,9 @@ compare.highlightColor.2=ハイライトカラー 2:
compare.document.1=ドキュメント 1
compare.document.2=ドキュメント 2
compare.submit=比較
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
compare.large.file.message=One or Both of the provided documents are too large to process
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
compare.complex.message=提供された文書の一方または両方が大きなファイルであるため、比較の精度が低下する可能性があります。
compare.large.file.message=提供された文書の1つまたは両方が大きすぎて処理できません
compare.no.text.message=選択したPDFの1つまたは両方にテキストコンテンツがありません。比較するには、テキストを含むPDFを選択してください。
#BookToPDF
BookToPDF.title=書籍やコミックをPDFに変換
@@ -803,8 +803,8 @@ BookToPDF.credit=calibreを使用
BookToPDF.submit=変換
#PDFToBook
PDFToBook.title=書籍をPDFに変換
PDFToBook.header=書籍をPDFに変換
PDFToBook.title=PDFを書籍に変換
PDFToBook.header=PDFを書籍に変換
PDFToBook.selectText.1=フォーマット
PDFToBook.credit=calibreを使用
PDFToBook.submit=変換
@@ -817,17 +817,17 @@ sign.draw=署名を書く
sign.text=テキスト入力
sign.clear=クリア
sign.add=追加
sign.saved=Saved Signatures
sign.save=Save Signature
sign.personalSigs=Personal Signatures
sign.sharedSigs=Shared Signatures
sign.noSavedSigs=No saved signatures found
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
sign.saved=保存された署名
sign.save=署名を保存
sign.personalSigs=個人署名
sign.sharedSigs=共有署名
sign.noSavedSigs=保存された署名が見つかりません
sign.addToAll=すべてのページに追加
sign.delete=削除
sign.first=最初のページ
sign.last=最後のページ
sign.next=次のページ
sign.previous=前のページ
#repair
repair.title=修復
@@ -944,29 +944,39 @@ pdfOrganiser.placeholder=(例:1,3,2または4-8,2,10-12または2n-1)
multiTool.title=PDFマルチツール
multiTool.header=PDFマルチツール
multiTool.uploadPrompts=ファイル名
multiTool.selectAll=Select All
multiTool.deselectAll=Deselect All
multiTool.selectPages=Page Select
multiTool.selectedPages=Selected Pages
multiTool.page=Page
multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected
multiTool.selectAll=すべて選択
multiTool.deselectAll=選択を解除
multiTool.selectPages=ページ選択
multiTool.selectedPages=選択したページ
multiTool.page=ページ
multiTool.deleteSelected=選択項目を削除
multiTool.downloadAll=エクスポート
multiTool.downloadSelected=選択項目をエクスポート
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
multiTool.rotateLeft=Rotate Left
multiTool.rotateRight=Rotate Right
multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
multiTool.insertPageBreak=改ページを挿入
multiTool.addFile=ファイルを追加
multiTool.rotateLeft=左回転
multiTool.rotateRight=右回転
multiTool.split=分割
multiTool.moveLeft=左に移動
multiTool.moveRight=右に移動
multiTool.delete=削除
multiTool.dragDropMessage=選択されたページ
multiTool.undo=元に戻す
multiTool.redo=やり直す
#decrypt
decrypt.passwordPrompt=このファイルはパスワードで保護されています。パスワードを入力してください:
decrypt.cancelled=PDFの操作がキャンセルされました: {0}
decrypt.noPassword=暗号化されたPDFにパスワードが指定されていません: {0}
decrypt.invalidPassword=正しいパスワードでもう一度お試しください。
decrypt.invalidPasswordHeader=PDFのパスワードが正しくないか、暗号化がサポートされていません: {0}
decrypt.unexpectedError=ファイルの処理中にエラーが発生しました。もう一度お試しください。
decrypt.serverError=復号化中にサーバーエラーが発生しました: {0}
decrypt.success=ファイルの暗号化が正常に完了しました。
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
multiTool-advert.message=この機能は、<a href="{0}">マルチツール</a>でもご利用いただけます。強化されたページごとのUIと追加機能についてはこちらをご覧ください。
#view pdf
viewPdf.title=PDFを表示
@@ -1056,6 +1066,7 @@ addPassword.submit=暗号化
#watermark
watermark.title=透かしの追加
watermark.header=透かしの追加
watermark.customColor=文字色のカスタム
watermark.selectText.1=透かしを追加するPDFを選択:
watermark.selectText.2=透かしのテキスト:
watermark.selectText.3=文字サイズ:
@@ -1121,8 +1132,8 @@ pdfToPDFA.header=PDFをPDF/Aに変換
pdfToPDFA.credit=本サービスはPDF/Aの変換にqpdfを使用しています。
pdfToPDFA.submit=変換
pdfToPDFA.tip=現在、一度に複数の入力に対して機能しません
pdfToPDFA.outputFormat=Output format
pdfToPDFA.pdfWithDigitalSignature=PDF にはデジタル署名が含まれています。これは次の手順で削除されます。
pdfToPDFA.outputFormat=出力形式
pdfToPDFA.pdfWithDigitalSignature=PDFにはデジタル署名が含まれています。これは次の手順で削除されます。
#PDFToWord
@@ -1227,8 +1238,8 @@ licenses.license=ライセンス
survey.nav=アンケート
survey.title=Stirling-PDFのアンケート
survey.description=Stirling-PDFには追跡機能がないため、Stirling-PDFをより良くするために皆様の意見を聞かせてください
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.changes=Stirling-PDFは前回の調査から変更されました。詳細についてはこちらのブログ投稿をご覧ください。
survey.changes2=これらの変更により私たちは有償のビジネスサポートと資金援助を受けています
survey.please=アンケートにご協力ください!
survey.disabled=(アンケートのポップアップは、次の更新では無効になりますが、ページの下部に表示されます。)
survey.button=アンケートに答える
@@ -1256,62 +1267,61 @@ removeImage.removeImage=画像の削除
removeImage.submit=画像を削除
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
splitByChapters.title=PDFをチャプターごとに分割
splitByChapters.header=PDFをチャプターごとに分割
splitByChapters.bookmarkLevel=ブックマークレベル
splitByChapters.includeMetadata=メタデータを含める
splitByChapters.allowDuplicates=重複を許可する
splitByChapters.desc.1=このツールは、チャプター構造に基づいてPDFファイルを複数のPDFに分割します。
splitByChapters.desc.2=ブックマークレベル:分割に使用するブックマークのレベルを選択します最上位レベルの場合は0、第2レベルの場合は1など
splitByChapters.desc.3=メタデータを含める:チェックすると、元のPDFのメタデータが各分割PDFに含まれます。
splitByChapters.desc.4=重複を許可:チェックすると同じページ上の複数のブックマークから個別のPDFを作成できます。
splitByChapters.submit=PDFを分割
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
fileChooser.click=クリック
fileChooser.or=または
fileChooser.dragAndDrop=ドラッグ&ドロップ
fileChooser.hoveredDragAndDrop=ファイルをここにドラッグ&ドロップ
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
releases.footer=リリース
releases.title=リリースノート
releases.header=リリースノート
releases.current.version=現在のリリース
releases.note=リリースノートは英語でのみで提供されています
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures
validateSignature.selectPDF=Select signed PDF file
validateSignature.submit=Validate Signatures
validateSignature.results=Validation Results
validateSignature.status=Status
validateSignature.signer=Signer
validateSignature.date=Date
validateSignature.reason=Reason
validateSignature.location=Location
validateSignature.noSignatures=No digital signatures found in this document
validateSignature.status.valid=Valid
validateSignature.status.invalid=Invalid
validateSignature.chain.invalid=Certificate chain validation failed - cannot verify signer's identity
validateSignature.trust.invalid=Certificate not in trust store - source cannot be verified
validateSignature.cert.expired=Certificate has expired
validateSignature.cert.revoked=Certificate has been revoked
validateSignature.signature.info=Signature Information
validateSignature.signature=Signature
validateSignature.signature.mathValid=Signature is mathematically valid BUT:
validateSignature.selectCustomCert=Custom Certificate File X.509 (Optional)
validateSignature.cert.info=Certificate Details
validateSignature.cert.issuer=Issuer
validateSignature.cert.subject=Subject
validateSignature.cert.serialNumber=Serial Number
validateSignature.cert.validFrom=Valid From
validateSignature.cert.validUntil=Valid Until
validateSignature.cert.algorithm=Algorithm
validateSignature.cert.keySize=Key Size
validateSignature.cert.version=Version
validateSignature.cert.keyUsage=Key Usage
validateSignature.cert.selfSigned=Self-Signed
validateSignature.cert.bits=bits
validateSignature.title=PDF署名の検証
validateSignature.header=デジタル署名の検証
validateSignature.selectPDF=署名済みPDFファイルを選択
validateSignature.submit=署名の検証
validateSignature.results=検証結果
validateSignature.status=状態
validateSignature.signer=署名者
validateSignature.date=日付
validateSignature.reason=理由
validateSignature.location=場所
validateSignature.noSignatures=この文書にはデジタル署名が見つかりません
validateSignature.status.valid=有効
validateSignature.status.invalid=無効
validateSignature.chain.invalid=証明書チェーンの検証に失敗しました - 署名者の身元を確認できません
validateSignature.trust.invalid=証明書が信頼ストアにありません - ソースを検証できません
validateSignature.cert.expired=証明書の有効期限が切れています
validateSignature.cert.revoked=証明書は取り消されました
validateSignature.signature.info=署名情報
validateSignature.signature=署名
validateSignature.signature.mathValid=署名は数学的には有効ですが:
validateSignature.selectCustomCert=カスタム証明書ファイル X.509 (オプション)
validateSignature.cert.info=証明書の詳細
validateSignature.cert.issuer=発行者
validateSignature.cert.subject=主題
validateSignature.cert.serialNumber=シリアルナンバー
validateSignature.cert.validFrom=有効開始日
validateSignature.cert.validUntil=有効期限
validateSignature.cert.algorithm=アルゴリズム
validateSignature.cert.keySize=キーサイズ
validateSignature.cert.version=バージョン
validateSignature.cert.keyUsage=キーの使用法
validateSignature.cert.selfSigned=自己署名
validateSignature.cert.bits=ビット

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=암호화
#watermark
watermark.title=워터마크 추가
watermark.header=워터마크 추가
watermark.customColor=사용자 정의 텍스트 색상
watermark.selectText.1=워터마크를 추가할 PDF 선택:
watermark.selectText.2=워터마크 텍스트:
watermark.selectText.3=폰트 크기:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Versleutelen
#watermark
watermark.title=Watermerk toevoegen
watermark.header=Watermerk toevoegen
watermark.customColor=Aangepaste tekstkleur
watermark.selectText.1=Selecteer PDF om watermerk toe te voegen:
watermark.selectText.2=Watermerk tekst:
watermark.selectText.3=Tekengrootte:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Krypter
#watermark
watermark.title=Legg til vannmerke
watermark.header=Legg til vannmerke
watermark.customColor=Tilpasset Tekstfarge
watermark.selectText.1=Velg PDF-fil å legge til vannmerke på:
watermark.selectText.2=Vannmerketekst:
watermark.selectText.3=Skriftstørrelse:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

12
src/main/resources/messages_pl_PL.properties Executable file → Normal file
View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Zablokuj
#watermark
watermark.title=Dodaj znak wodny
watermark.header=Dodaj znak wodny
watermark.customColor=Własny kolor tekstu
watermark.selectText.1=Wybierz dokument PDF, do którego chcesz dodać znak wodny:
watermark.selectText.2=Treść znaku wodnego:
watermark.selectText.3=Rozmiar czcionki:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

File diff suppressed because it is too large Load Diff

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Proteger
#watermark
watermark.title=Adicionar Marca d'Água
watermark.header=Adicionar Marca d'Água
watermark.customColor=Personalizar a cor do texto
watermark.selectText.1=Seleccione o PDF para Adicionar a Marca d'Água
watermark.selectText.2=Texto da Marca d'Água
watermark.selectText.3=Tamanho da Fonte
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Criptează
#watermark
watermark.title=Adaugă Filigran
watermark.header=Adaugă Filigran
watermark.customColor=Culoare Text Personalizată
watermark.selectText.1=Selectează PDF-ul la care să adaugi filigranul:
watermark.selectText.2=Textul Filigranului:
watermark.selectText.3=Mărimea fontului:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Шифровать
#watermark
watermark.title=Добавить водяной знак
watermark.header=Добавить водяной знак
watermark.customColor=Настроенный цвет текста
watermark.selectText.1=Выберите PDF, чтобы добавить водяной знак:
watermark.selectText.2=Текст водяного знака:
watermark.selectText.3=Размер шрифта:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Zašifrovať
#watermark
watermark.title=Pridať vodotlač
watermark.header=Pridať vodotlač
watermark.customColor=Vlastná farba textu
watermark.selectText.1=Vyberte PDF, do ktorého chcete pridať vodotlač:
watermark.selectText.2=Text vodotlače:
watermark.selectText.3=Veľkosť písma:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Enkriptuj
#watermark
watermark.title=Dodaj vodeni žig
watermark.header=Dodaj vodeni žig
watermark.customColor=Custom Text Color
watermark.selectText.1=Izaberite PDF za dodavanje vodenog žiga:
watermark.selectText.2=Tekst vodenog žiga:
watermark.selectText.3=Veličina fonta:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Kryptera
#watermark
watermark.title=Lägg till vattenstämpel
watermark.header=Lägg till vattenstämpel
watermark.customColor=Anpassad textfärg
watermark.selectText.1=Välj PDF för att lägga till vattenstämpel till:
watermark.selectText.2=Vattenmärkestext:
watermark.selectText.3=Teckenstorlek:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=เข้ารหัส
#watermark
watermark.title=เพิ่มลายน้ำ
watermark.header=เพิ่มลายน้ำ
watermark.customColor=สีข้อความที่กำหนดเอง
watermark.selectText.1=เลือก PDF เพื่อเพิ่มลายน้ำ:
watermark.selectText.2=ข้อความลายน้ำ:
watermark.selectText.3=ขนาดฟอนต์:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Şifrele
#watermark
watermark.title=Filigran Ekle
watermark.header=Filigran Ekle
watermark.customColor=Özel Metin Rengi
watermark.selectText.1=Filigran eklemek için PDF seçin:
watermark.selectText.2=Filigran Metni:
watermark.selectText.3=Yazı Boyutu:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Шифрувати
#watermark
watermark.title=Додати водяний знак
watermark.header=Додати водяний знак
watermark.customColor=Користувацький колір тексту
watermark.selectText.1=Виберіть PDF, щоб додати водяний знак:
watermark.selectText.2=Текст водяного знаку:
watermark.selectText.3=Розмір шрифту:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=Mã hóa
#watermark
watermark.title=Thêm hình mờ
watermark.header=Thêm hình mờ
watermark.customColor=Màu văn bản tùy chỉnh
watermark.selectText.1=Chọn PDF để thêm hình mờ:
watermark.selectText.2=Văn bản hình mờ:
watermark.selectText.3=Cỡ chữ:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

File diff suppressed because it is too large Load Diff

View File

@@ -965,6 +965,16 @@ multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#decrypt
decrypt.passwordPrompt=This file is password-protected. Please enter the password:
decrypt.cancelled=Operation cancelled for PDF: {0}
decrypt.noPassword=No password provided for encrypted PDF: {0}
decrypt.invalidPassword=Please try again with the correct password.
decrypt.invalidPasswordHeader=Incorrect password or unsupported encryption for PDF: {0}
decrypt.unexpectedError=There was an error processing the file. Please try again.
decrypt.serverError=Server error while decrypting: {0}
decrypt.success=File decrypted successfully.
#multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1056,6 +1066,7 @@ addPassword.submit=加密
#watermark
watermark.title=新增浮水印
watermark.header=新增浮水印
watermark.customColor=自訂文字顏色
watermark.selectText.1=選擇要新增浮水印的 PDF
watermark.selectText.2=浮水印文字:
watermark.selectText.3=字型大小:
@@ -1280,7 +1291,6 @@ releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English
#Validate Signature
#Validate Signature
validateSignature.title=Validate PDF Signatures
validateSignature.header=Validate Digital Signatures

View File

@@ -13,7 +13,7 @@
security:
enableLogin: false # set to 'true' to enable login
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production)
csrfDisabled: false # set to 'true' to disable CSRF protection (not recommended for production)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
@@ -102,7 +102,8 @@ metrics:
AutomaticallyGenerated:
key: example
UUID: example
appVersion: 0.35.0
processExecutor:
sessionLimit: # Process executor instances limits
libreOfficeSessionLimit: 1

View File

@@ -172,6 +172,13 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.code.gson:gson",
"moduleUrl": "https://github.com/google/gson",
"moduleVersion": "2.11.0",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.errorprone:error_prone_annotations",
"moduleUrl": "https://errorprone.info/error_prone_annotations",
@@ -591,6 +598,34 @@
"moduleLicense": "GPL2 w/ CPE",
"moduleLicenseUrl": "https://oss.oracle.com/licenses/CDDL+GPL-1.1"
},
{
"moduleName": "me.friwi:gluegen-rt",
"moduleUrl": "http://jogamp.org/gluegen/www/",
"moduleVersion": "v2.4.0-rc-20210111",
"moduleLicense": "BSD-4 License",
"moduleLicenseUrl": "http://www.spdx.org/licenses/BSD-4-Clause"
},
{
"moduleName": "me.friwi:jcef-api",
"moduleUrl": "https://bitbucket.org/chromiumembedded/java-cef/",
"moduleVersion": "jcef-99c2f7a+cef-127.3.1+g6cbb30e+chromium-127.0.6533.100",
"moduleLicense": "BSD License",
"moduleLicenseUrl": "https://bitbucket.org/chromiumembedded/java-cef/src/master/LICENSE.txt"
},
{
"moduleName": "me.friwi:jcefmaven",
"moduleUrl": "https://github.com/jcefmaven/jcefmaven/",
"moduleVersion": "127.3.1",
"moduleLicense": "Apache-2.0 License",
"moduleLicenseUrl": "https://github.com/jcefmaven/jcefmaven/blob/master/LICENSE"
},
{
"moduleName": "me.friwi:jogl-all",
"moduleUrl": "http://jogamp.org/jogl/www/",
"moduleVersion": "v2.4.0-rc-20210111",
"moduleLicense": "Ubuntu Font Licence 1.0",
"moduleLicenseUrl": "http://font.ubuntu.com/ufl/ubuntu-font-licence-1.0.txt"
},
{
"moduleName": "net.bytebuddy:byte-buddy",
"moduleVersion": "1.15.10",
@@ -631,6 +666,13 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.commons:commons-compress",
"moduleUrl": "https://commons.apache.org/proper/commons-compress/",
"moduleVersion": "1.21",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.commons:commons-csv",
"moduleUrl": "https://commons.apache.org/proper/commons-csv/",
@@ -1079,6 +1121,30 @@
"moduleLicense": "Eclipse Public License, Version 2.0",
"moduleLicenseUrl": "https://github.com/locationtech/jts/blob/master/LICENSE_EPLv2.txt"
},
{
"moduleName": "org.openjfx:javafx-base",
"moduleVersion": "21",
"moduleLicense": "GPLv2+CE",
"moduleLicenseUrl": "https://openjdk.java.net/legal/gplv2+ce.html"
},
{
"moduleName": "org.openjfx:javafx-controls",
"moduleVersion": "21",
"moduleLicense": "GPLv2+CE",
"moduleLicenseUrl": "https://openjdk.java.net/legal/gplv2+ce.html"
},
{
"moduleName": "org.openjfx:javafx-graphics",
"moduleVersion": "21",
"moduleLicense": "GPLv2+CE",
"moduleLicenseUrl": "https://openjdk.java.net/legal/gplv2+ce.html"
},
{
"moduleName": "org.openjfx:javafx-swing",
"moduleVersion": "21",
"moduleLicense": "GPLv2+CE",
"moduleLicenseUrl": "https://openjdk.java.net/legal/gplv2+ce.html"
},
{
"moduleName": "org.opensaml:opensaml-core",
"moduleVersion": "4.3.2",
@@ -1479,7 +1545,7 @@
},
{
"moduleName": "org.thymeleaf.extras:thymeleaf-extras-springsecurity5",
"moduleVersion": "3.1.2.RELEASE",
"moduleVersion": "3.1.3.RELEASE",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
@@ -1491,7 +1557,7 @@
},
{
"moduleName": "org.thymeleaf:thymeleaf-spring5",
"moduleVersion": "3.1.2.RELEASE",
"moduleVersion": "3.1.3.RELEASE",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},

View File

@@ -65,6 +65,7 @@
overflow: hidden;
margin: -20px;
padding: 20px;
box-sizing:content-box;
}
.feature-group-container.animated-group {

Binary file not shown.

View File

@@ -0,0 +1,219 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-ir" viewBox="0 0 640 480">
<defs>
<clipPath id="ir-a">
<path fill-opacity=".7" d="M-85.3 0h682.7v512H-85.3z"/>
</clipPath>
</defs>
<g fill-rule="evenodd" clip-path="url(#ir-a)" transform="translate(80)scale(.9375)">
<path fill="#fff" d="M-192 0h896v512h-896z"/>
<path fill="#da0000" d="M-192 343.8h896V512h-896z"/>
<g fill="#fff" stroke-width="1pt">
<path d="M-21.6 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm41.9 0v3.3h-9.8v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M52.4 367.7v3.4H33.8v-3.4zm-34.6-7.9H21v11.3h-3.3z"/>
<path d="M49.6 351H53v20h-3.4zm-8.4 0h3.3v20h-3.3zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M17.8 359.9H21V371h-3.3z"/>
<path d="M17.8 359.9H21V371h-3.3z"/>
<path d="M17.8 359.9H21V371h-3.3zm-39.3 0h3.3V371h-3.3zm28.8 0h3.4V371H7.3zm-14.3 0h3.4V371H-7z"/>
<path d="M9.6 367.7v3.4H-5.5v-3.4zm1-8.7v3.4H1V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M-102.2 351h49v3.3h-49zm7.3 16.8h3.4v3.3H-95zm41.9 0v3.3h-9.8v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M-28.2 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.3v11.3h-3.3z"/>
<path d="M-31 351h3.4v20H-31zm-8.4 0h3.3v20h-3.3zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M-62.8 359.9h3.3V371h-3.3z"/>
<path d="M-62.8 359.9h3.3V371h-3.3z"/>
<path d="M-62.8 359.9h3.3V371h-3.3zm-39.3 0h3.3V371h-3.3zm28.8 0h3.3V371h-3.3zm-14.3 0h3.4V371h-3.4z"/>
<path d="M-71 367.7v3.4h-15v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M58.3 351h49v3.3h-49zm7.3 16.8H69v3.3h-3.4zm41.9 0v3.3h-9.8v-3.4zm5.3-16.8h3.4v20h-3.4z"/>
<path d="M132.3 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M129.5 351h3.4v20h-3.4zm-8.4 0h3.4v20H121zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M97.7 359.9h3.4V371h-3.4z"/>
<path d="M97.7 359.9h3.4V371h-3.4z"/>
<path d="M97.7 359.9h3.4V371h-3.4zm-39.3 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371h-3.4z"/>
<path d="M89.6 367.7v3.4H74.4v-3.4zm1-8.7v3.4H81V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M622.7 351h49v3.3h-49zm7.3 16.8h3.4v3.3H630zm41.9 0v3.3H662v-3.4zm5.3-16.8h3.3v20h-3.4z"/>
<path d="M696.7 367.7v3.4H678v-3.4zm-34.6-7.9h3.4v11.3H662z"/>
<path d="M694 351h3.3v20h-3.4zm-8.5 0h3.4v20h-3.4zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M662 359.9h3.5V371H662z"/>
<path d="M662 359.9h3.5V371H662z"/>
<path d="M662 359.9h3.5V371H662zm-39.2 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371h-3.4z"/>
<path d="M654 367.7v3.4h-15.2v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M138.7 351h49.1v3.3h-49zm7.4 16.8h3.3v3.3h-3.3zm41.8 0v3.3h-9.8v-3.4zm5.3-16.8h3.4v20h-3.4z"/>
<path d="M212.8 367.7v3.4h-18.6v-3.4zm-34.7-7.9h3.4v11.3h-3.4z"/>
<path d="M210 351h3.4v20H210zm-8.5 0h3.4v20h-3.4zm-44.8 8v3.4h-17.9V359zm39.3 0v3.4h-17.9V359z"/>
<path d="M178.1 359.9h3.4V371h-3.4z"/>
<path d="M178.1 359.9h3.4V371h-3.4z"/>
<path d="M178.1 359.9h3.4V371h-3.4zm-39.3 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.2 0h3.3V371h-3.3z"/>
<path d="M170 367.7v3.4h-15.1v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M219.5 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm41.9 0v3.3h-9.8v-3.4zM274 351h3.3v20H274z"/>
<path d="M293.5 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M290.7 351h3.4v20h-3.4zm-8.4 0h3.4v20h-3.4zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M258.9 359.9h3.4V371h-3.4z"/>
<path d="M258.9 359.9h3.4V371h-3.4z"/>
<path d="M258.9 359.9h3.4V371h-3.4zm-39.3 0h3.3V371h-3.3zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371H234z"/>
<path d="M250.8 367.7v3.4h-15.2v-3.4zm1-8.7v3.4H242V359z"/>
</g>
<path fill="#239f40" d="M-192 0h896v168.2h-896z"/>
<g fill="#fff" stroke-width="1pt">
<path d="M300.7 351h49v3.3h-49zm7.3 16.8h3.4v3.3H308zm41.9 0v3.3H340v-3.4zm5.3-16.8h3.3v20h-3.3z"/>
<path d="M374.7 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3H340z"/>
<path d="M372 351h3.3v20H372zm-8.5 0h3.4v20h-3.4zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M340 359.9h3.5V371H340z"/>
<path d="M340 359.9h3.5V371H340z"/>
<path d="M340 359.9h3.5V371H340zm-39.2 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371h-3.4z"/>
<path d="M332 367.7v3.4h-15.2v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M381.4 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm42 0v3.3h-9.9v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M455.4 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M452.7 351h3.3v20h-3.3zm-8.5 0h3.4v20h-3.4zm-44.8 8v3.4h-17.9V359zm39.3 0v3.4h-17.9V359z"/>
<path d="M420.8 359.9h3.4V371h-3.4z"/>
<path d="M420.8 359.9h3.4V371h-3.4z"/>
<path d="M420.8 359.9h3.4V371h-3.4zm-39.3 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371h-3.3z"/>
<path d="M412.7 367.7v3.4h-15.1v-3.4zm1-8.7v3.4H404V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M462.2 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm41.9 0v3.3h-9.8v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M536.2 367.7v3.4h-18.6v-3.4zm-34.7-7.9h3.4v11.3h-3.4z"/>
<path d="M533.4 351h3.4v20h-3.4zm-8.4 0h3.3v20H525zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M501.6 359.9h3.3V371h-3.3z"/>
<path d="M501.6 359.9h3.3V371h-3.3z"/>
<path d="M501.6 359.9h3.3V371h-3.3zm-39.4 0h3.4V371h-3.4zm28.9 0h3.3V371h-3.3zm-14.3 0h3.4V371h-3.4z"/>
<path d="M493.4 367.7v3.4h-15.1v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M543.4 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm41.9 0v3.3h-9.8v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M617.4 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.3v11.3h-3.3z"/>
<path d="M614.6 351h3.4v20h-3.4zm-8.4 0h3.3v20h-3.3zm-44.8 8v3.4h-18V359zm39.3 0v3.4h-18V359z"/>
<path d="M582.8 359.9h3.3V371h-3.3z"/>
<path d="M582.8 359.9h3.3V371h-3.3z"/>
<path d="M582.8 359.9h3.3V371h-3.3zm-39.3 0h3.3V371h-3.3zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371H558z"/>
<path d="M574.6 367.7v3.4h-15.1v-3.4zm1-8.7v3.4H566V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M-183.8 351h49v3.3h-49zm7.3 16.8h3.4v3.3h-3.4zm42 0v3.3h-9.9v-3.4zm5.2-16.8h3.4v20h-3.4z"/>
<path d="M-109.8 367.7v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M-112.5 351h3.3v20h-3.3zm-8.5 0h3.4v20h-3.4zm-44.8 8v3.4h-17.9V359zm39.3 0v3.4h-17.9V359z"/>
<path d="M-144.4 359.9h3.4V371h-3.4z"/>
<path d="M-144.4 359.9h3.4V371h-3.4z"/>
<path d="M-144.4 359.9h3.4V371h-3.4zm-39.3 0h3.4V371h-3.4zm28.8 0h3.4V371h-3.4zm-14.3 0h3.4V371h-3.4z"/>
<path d="M-152.5 367.7v3.4h-15.2v-3.4zm1-8.7v3.4h-9.6V359z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M-21.6 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm41.9-.2v3.4h-9.8v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M52.4 160.2v3.4H33.8v-3.4zm-34.6-7.9H21v11.3h-3.3z"/>
<path d="M49.6 143.4H53v20.2h-3.4zm-8.4 0h3.3v20.2h-3.3zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M17.8 152.3H21v11.3h-3.3z"/>
<path d="M17.8 152.3H21v11.3h-3.3z"/>
<path d="M17.8 152.3H21v11.3h-3.3zm-39.3 0h3.3v11.3h-3.3zm28.8 0h3.4v11.3H7.3zm-14.3 0h3.4v11.3H-7z"/>
<path d="M9.6 160.2v3.4H-5.5v-3.4zm1-8.7v3.3H1v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M-102.2 143.4h49v3.4h-49zm7.3 17h3.4v3.2H-95zm41.9-.2v3.4h-9.8v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M-28.2 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.3v11.3h-3.3z"/>
<path d="M-31 143.4h3.4v20.2H-31zm-8.4 0h3.3v20.2h-3.3zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M-62.8 152.3h3.3v11.3h-3.3z"/>
<path d="M-62.8 152.3h3.3v11.3h-3.3z"/>
<path d="M-62.8 152.3h3.3v11.3h-3.3zm-39.3 0h3.3v11.3h-3.3zm28.8 0h3.3v11.3h-3.3zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M-71 160.2v3.4h-15v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M58.3 143.4h49v3.4h-49zm7.3 17H69v3.2h-3.4zm41.9-.2v3.4h-9.8v-3.4zm5.3-16.8h3.4v20.2h-3.4z"/>
<path d="M132.3 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M129.5 143.4h3.4v20.2h-3.4zm-8.4 0h3.4v20.2H121zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M97.7 152.3h3.4v11.3h-3.4z"/>
<path d="M97.7 152.3h3.4v11.3h-3.4z"/>
<path d="M97.7 152.3h3.4v11.3h-3.4zm-39.3 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M89.6 160.2v3.4H74.4v-3.4zm1-8.7v3.3H81v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M622.7 143.4h49v3.4h-49zm7.3 17h3.4v3.2H630zm41.9-.2v3.4H662v-3.4zm5.3-16.8h3.3v20.2h-3.4z"/>
<path d="M696.7 160.2v3.4H678v-3.4zm-34.6-7.9h3.4v11.3H662z"/>
<path d="M694 143.4h3.3v20.2h-3.4zm-8.5 0h3.4v20.2h-3.4zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M662 152.3h3.5v11.3H662z"/>
<path d="M662 152.3h3.5v11.3H662z"/>
<path d="M662 152.3h3.5v11.3H662zm-39.2 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M654 160.2v3.4h-15.2v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M138.7 143.4h49.1v3.4h-49zm7.4 17h3.3v3.2h-3.3zm41.8-.2v3.4h-9.8v-3.4zm5.3-16.8h3.4v20.2h-3.4z"/>
<path d="M212.8 160.2v3.4h-18.6v-3.4zm-34.7-7.9h3.4v11.3h-3.4z"/>
<path d="M210 143.4h3.4v20.2H210zm-8.5 0h3.4v20.2h-3.4zm-44.8 8v3.4h-17.9v-3.3zm39.3 0v3.4h-17.9v-3.3z"/>
<path d="M178.1 152.3h3.4v11.3h-3.4z"/>
<path d="M178.1 152.3h3.4v11.3h-3.4z"/>
<path d="M178.1 152.3h3.4v11.3h-3.4zm-39.3 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.2 0h3.3v11.3h-3.3z"/>
<path d="M170 160.2v3.4h-15.1v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M219.5 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm41.9-.2v3.4h-9.8v-3.4zm5.3-16.8h3.3v20.2H274z"/>
<path d="M293.5 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M290.7 143.4h3.4v20.2h-3.4zm-8.4 0h3.4v20.2h-3.4zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M258.9 152.3h3.4v11.3h-3.4z"/>
<path d="M258.9 152.3h3.4v11.3h-3.4z"/>
<path d="M258.9 152.3h3.4v11.3h-3.4zm-39.3 0h3.3v11.3h-3.3zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3H234z"/>
<path d="M250.8 160.2v3.4h-15.2v-3.4zm1-8.7v3.3H242v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M300.7 143.4h49v3.4h-49zm7.3 17h3.4v3.2H308zm41.9-.2v3.4H340v-3.4zm5.3-16.8h3.3v20.2h-3.3z"/>
<path d="M374.7 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3H340z"/>
<path d="M372 143.4h3.3v20.2H372zm-8.5 0h3.4v20.2h-3.4zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M340 152.3h3.5v11.3H340z"/>
<path d="M340 152.3h3.5v11.3H340z"/>
<path d="M340 152.3h3.5v11.3H340zm-39.2 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M332 160.2v3.4h-15.2v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M381.4 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm42-.2v3.4h-9.9v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M455.4 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M452.7 143.4h3.3v20.2h-3.3zm-8.5 0h3.4v20.2h-3.4zm-44.8 8v3.4h-17.9v-3.3zm39.3 0v3.4h-17.9v-3.3z"/>
<path d="M420.8 152.3h3.4v11.3h-3.4z"/>
<path d="M420.8 152.3h3.4v11.3h-3.4z"/>
<path d="M420.8 152.3h3.4v11.3h-3.4zm-39.3 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3h-3.3z"/>
<path d="M412.7 160.2v3.4h-15.1v-3.4zm1-8.7v3.3H404v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M462.2 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm41.9-.2v3.4h-9.8v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M536.2 160.2v3.4h-18.6v-3.4zm-34.7-7.9h3.4v11.3h-3.4z"/>
<path d="M533.4 143.4h3.4v20.2h-3.4zm-8.4 0h3.3v20.2H525zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M501.6 152.3h3.3v11.3h-3.3z"/>
<path d="M501.6 152.3h3.3v11.3h-3.3z"/>
<path d="M501.6 152.3h3.3v11.3h-3.3zm-39.4 0h3.4v11.3h-3.4zm28.9 0h3.3v11.3h-3.3zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M493.4 160.2v3.4h-15.1v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M543.4 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm41.9-.2v3.4h-9.8v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M617.4 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.3v11.3h-3.3z"/>
<path d="M614.6 143.4h3.4v20.2h-3.4zm-8.4 0h3.3v20.2h-3.3zm-44.8 8v3.4h-18v-3.3zm39.3 0v3.4h-18v-3.3z"/>
<path d="M582.8 152.3h3.3v11.3h-3.3z"/>
<path d="M582.8 152.3h3.3v11.3h-3.3z"/>
<path d="M582.8 152.3h3.3v11.3h-3.3zm-39.3 0h3.3v11.3h-3.3zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3H558z"/>
<path d="M574.6 160.2v3.4h-15.1v-3.4zm1-8.7v3.3H566v-3.3z"/>
</g>
<g fill="#fff" stroke-width="1pt">
<path d="M-183.8 143.4h49v3.4h-49zm7.3 17h3.4v3.2h-3.4zm42-.2v3.4h-9.9v-3.4zm5.2-16.8h3.4v20.2h-3.4z"/>
<path d="M-109.8 160.2v3.4h-18.6v-3.4zm-34.6-7.9h3.4v11.3h-3.4z"/>
<path d="M-112.5 143.4h3.3v20.2h-3.3zm-8.5 0h3.4v20.2h-3.4zm-44.8 8v3.4h-17.9v-3.3zm39.3 0v3.4h-17.9v-3.3z"/>
<path d="M-144.4 152.3h3.4v11.3h-3.4z"/>
<path d="M-144.4 152.3h3.4v11.3h-3.4z"/>
<path d="M-144.4 152.3h3.4v11.3h-3.4zm-39.3 0h3.4v11.3h-3.4zm28.8 0h3.4v11.3h-3.4zm-14.3 0h3.4v11.3h-3.4z"/>
<path d="M-152.5 160.2v3.4h-15.2v-3.4zm1-8.7v3.3h-9.6v-3.3z"/>
</g>
<path fill="#d90000" d="M-68.8 339.5h6V350h-6zm160.5 0h6V350h-6zm-283.7 0h6V350h-6zm81.5 0h6V350h-6zm80.9 0h6V350h-6zm40 0h6V350h-6zm40.9 0h6V350h-6zm80.4 0h6V350h-6zm203 0h6.1V350h-6zm-162.1 0h6V350h-6zm40 0h6V350h-6zm40.5 0h6V350h-6zm40.4 0h6V350h-6zm323.2 0h6V350h-6zm-242.7 0h6V350h-6zm40.8 0h6V350h-6zm41.3 0h6V350h-6zm38.8 0h6V350h-6zm41.3 0h6V350h-6zm40.4 0h6V350h-6zm119.7 0h6V350h-6zm-38.8 0h6V350h-6zm-808.9 0h6V350h-6z"/>
<path fill="#239e3f" d="M-68.8 162.6h6v10.5h-6zm160.5 0h6v10.5h-6zm-283.7 0h6v10.5h-6zm81.5 0h6v10.5h-6zm80.9 0h6v10.5h-6zm40 0h6v10.5h-6zm40.9 0h6v10.5h-6zm80.4 0h6v10.5h-6zm203 0h6.1v10.5h-6zm-162.1 0h6v10.5h-6zm40 0h6v10.5h-6zm40.5 0h6v10.5h-6zm40.4 0h6v10.5h-6zm323.2 0h6v10.5h-6zm-242.7 0h6v10.5h-6zm40.8 0h6v10.5h-6zm41.3 0h6v10.5h-6zm38.8 0h6v10.5h-6zm41.3 0h6v10.5h-6zm40.4 0h6v10.5h-6zm119.7 0h6v10.5h-6zm-38.8 0h6v10.5h-6zm-808.9 0h6v10.5h-6z"/>
<g fill="#da0000">
<path d="M279.8 197.5c8.4 10.4 34.5 67.6-15.7 105.2-23.7 17.8-9 18.6-8.3 21.6 38-20.1 50.3-47.5 50-72-.2-24.4-13.2-46-26-54.8"/>
<path d="M284.8 194.8a73.3 73.3 0 0 1 15.7 112.4c27.2-6 62-86.4-15.7-112.4m-57.6 0a73.3 73.3 0 0 0-15.6 112.4c-27.3-6-62-86.4 15.6-112.4"/>
<path d="M232.2 197.5c-8.4 10.4-34.5 67.6 15.7 105.2 23.6 17.8 9 18.6 8.3 21.6-38-20.1-50.3-47.5-50-72 .2-24.4 13.2-46 26-54.8"/>
<path d="M304.2 319.1c-14.9.2-33.6-2-47.5-9.3 2.3 4.5 4.2 7.3 6.5 11.7 13.2 1.3 31.5 2.8 41-2.4m-95 0c14.9.2 33.6-2 47.5-9.3-2.3 4.5-4.2 7.3-6.5 11.7-13.2 1.3-31.5 2.8-41-2.4m27.3-138.7c3 8 10.9 9.2 19.3 4.5 6.2 3.6 15.7 3.9 19-4.1 2.5 19.8-18.3 15-19 11.2-7.8 7.5-22.2 3.2-19.3-11.6"/>
<path d="m256.4 331.6 7.8-9 1.1-120.1-9.3-8.2-9.3 7.8 1.9 121z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,131 @@
export class DecryptFile {
async decryptFile(file, requiresPassword) {
try {
async function getCsrfToken() {
const cookieValue = document.cookie
.split('; ')
.find((row) => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (cookieValue) {
return cookieValue;
}
const csrfElement = document.querySelector('input[name="_csrf"]');
return csrfElement ? csrfElement.value : null;
}
const csrfToken = await getCsrfToken();
const formData = new FormData();
formData.append('fileInput', file);
if (requiresPassword) {
const password = prompt(`${window.decrypt.passwordPrompt}`);
if (password === null) {
// User cancelled
console.error(`Password prompt cancelled for PDF: ${file.name}`);
return null; // No file to return
}
if (!password) {
// No password provided
console.error(`No password provided for encrypted PDF: ${file.name}`);
this.showErrorBanner(
`${window.decrypt.noPassword.replace('{0}', file.name)}`,
'',
`${window.decrypt.unexpectedError}`
);
return null; // No file to return
}
formData.append('password', password);
}
// Send decryption request
const response = await fetch('/api/v1/security/remove-password', {
method: 'POST',
body: formData,
headers: csrfToken ? {'X-XSRF-TOKEN': csrfToken} : undefined,
});
if (response.ok) {
const decryptedBlob = await response.blob();
this.removeErrorBanner();
return new File([decryptedBlob], file.name, {type: 'application/pdf'});
} else {
const errorText = await response.text();
console.error(`${window.decrypt.invalidPassword} ${errorText}`);
this.showErrorBanner(
`${window.decrypt.invalidPassword}`,
errorText,
`${window.decrypt.invalidPasswordHeader.replace('{0}', file.name)}`
);
return null; // No file to return
}
} catch (error) {
// Handle network or unexpected errors
console.error(`Failed to decrypt PDF: ${file.name}`, error);
this.showErrorBanner(
`${window.decrypt.unexpectedError.replace('{0}', file.name)}`,
`${error.message || window.decrypt.unexpectedError}`,
error
);
return null; // No file to return
}
}
async checkFileEncrypted(file) {
try {
if (file.type !== 'application/pdf') {
return {isEncrypted: false, requiresPassword: false};
}
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const arrayBuffer = await file.arrayBuffer();
const arrayBufferForPdfLib = arrayBuffer.slice(0);
const loadingTask = pdfjsLib.getDocument({
data: arrayBuffer,
});
await loadingTask.promise;
try {
//Uses PDFLib.PDFDocument to check if unpassworded but encrypted
const pdfDoc = await PDFLib.PDFDocument.load(arrayBufferForPdfLib);
return {isEncrypted: false, requiresPassword: false};
} catch (error) {
if (error.message.includes('Input document to `PDFDocument.load` is encrypted')) {
return {isEncrypted: true, requiresPassword: false};
}
console.error('Error checking encryption:', error);
throw new Error('Failed to determine if the file is encrypted.');
}
} catch (error) {
if (error.name === 'PasswordException') {
if (error.code === pdfjsLib.PasswordResponses.NEED_PASSWORD) {
return {isEncrypted: true, requiresPassword: true};
} else if (error.code === pdfjsLib.PasswordResponses.INCORRECT_PASSWORD) {
return {isEncrypted: true, requiresPassword: false};
}
}
console.error('Error checking encryption:', error);
throw new Error('Failed to determine if the file is encrypted.');
}
}
showErrorBanner(message, stackTrace, error) {
const errorContainer = document.getElementById('errorContainer');
errorContainer.style.display = 'block'; // Display the banner
errorContainer.querySelector('.alert-heading').textContent = error;
errorContainer.querySelector('p').textContent = message;
document.querySelector('#traceContent').textContent = stackTrace;
}
removeErrorBanner() {
const errorContainer = document.getElementById('errorContainer');
errorContainer.style.display = 'none'; // Hide the banner
errorContainer.querySelector('.alert-heading').textContent = '';
errorContainer.querySelector('p').textContent = '';
document.querySelector('#traceContent').textContent = '';
}
}

View File

@@ -0,0 +1,27 @@
document.getElementById('download-pdf').addEventListener('click', async () => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
let decryptedFile = modifiedPdf;
let isEncrypted = false;
let requiresPassword = false;
await this.decryptFile
.checkFileEncrypted(decryptedFile)
.then((result) => {
isEncrypted = result.isEncrypted;
requiresPassword = result.requiresPassword;
})
.catch((error) => {
console.error(error);
});
if (decryptedFile.type === 'application/pdf' && isEncrypted) {
decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
if (!decryptedFile) {
throw new Error('File decryption failed.');
}
}
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_signed.pdf';
link.click();
});

View File

@@ -42,7 +42,7 @@
event.preventDefault();
firstErrorOccurred = false;
const url = this.action;
const files = $('#fileInput-input')[0].files;
let files = $('#fileInput-input')[0].files;
const formData = new FormData(this);
const submitButton = document.getElementById('submitBtn');
const showGameBtn = document.getElementById('show-game-btn');
@@ -71,6 +71,16 @@
}, 5000);
try {
if (!url.includes('remove-password')) {
// Check if any PDF files are encrypted and handle decryption if necessary
const decryptedFiles = await checkAndDecryptFiles(url, files);
files = decryptedFiles;
// Append decrypted files to formData
decryptedFiles.forEach((file, index) => {
formData.set(`fileInput`, file);
});
}
submitButton.textContent = 'Processing...';
submitButton.disabled = true;
@@ -82,7 +92,7 @@
}
}
clearFileInput();
//clearFileInput();
clearTimeout(timeoutId);
if (showGameBtn) {
showGameBtn.style.display = 'none';
@@ -133,6 +143,98 @@
}
}
async function checkAndDecryptFiles(url, files) {
const decryptedFiles = [];
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
// Extract the base URL
const baseUrl = new URL(url);
let removePasswordUrl = `${baseUrl.origin}`;
// Check if there's a path before /api/
const apiIndex = baseUrl.pathname.indexOf('/api/');
if (apiIndex > 0) {
removePasswordUrl += baseUrl.pathname.substring(0, apiIndex);
}
// Append the new endpoint
removePasswordUrl += '/api/v1/security/remove-password';
console.log(`Remove password URL: ${removePasswordUrl}`);
for (const file of files) {
console.log(`Processing file: ${file.name}`);
if (file.type !== 'application/pdf') {
console.log(`Skipping non-PDF file: ${file.name}`);
decryptedFiles.push(file);
continue;
}
try {
const arrayBuffer = await file.arrayBuffer();
const loadingTask = pdfjsLib.getDocument({data: arrayBuffer});
console.log(`Attempting to load PDF: ${file.name}`);
const pdf = await loadingTask.promise;
console.log(`File is not encrypted: ${file.name}`);
decryptedFiles.push(file); // If no error, file is not encrypted
} catch (error) {
if (error.name === 'PasswordException' && error.code === 1) {
console.log(`PDF requires password: ${file.name}`, error);
console.log(`Attempting to remove password from PDF: ${file.name} with password.`);
const password = prompt(`${window.translations.decrypt.passwordPrompt}`);
if (!password) {
console.error(`No password provided for encrypted PDF: ${file.name}`);
showErrorBanner(
`${window.translations.decrypt.noPassword.replace('{0}', file.name)}`,
`${window.translations.decrypt.unexpectedError}`
);
throw error;
}
try {
// Prepare FormData for the decryption request
const formData = new FormData();
formData.append('fileInput', file);
formData.append('password', password);
// Use handleSingleDownload to send the request
const decryptionResult = await fetch(removePasswordUrl, {method: 'POST', body: formData});
if (decryptionResult && decryptionResult.blob) {
const decryptedBlob = await decryptionResult.blob();
const decryptedFile = new File([decryptedBlob], file.name, {type: 'application/pdf'});
/* // Create a link element to download the file
const link = document.createElement('a');
link.href = URL.createObjectURL(decryptedBlob);
link.download = 'test.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
*/
decryptedFiles.push(decryptedFile);
console.log(`Successfully decrypted PDF: ${file.name}`);
} else {
throw new Error('Decryption failed: No valid response from server');
}
} catch (decryptError) {
console.error(`Failed to decrypt PDF: ${file.name}`, decryptError);
showErrorBanner(
`${window.translations.invalidPasswordHeader.replace('{0}', file.name)}`,
`${window.translations.invalidPassword}`
);
throw decryptError;
}
} else {
console.log(`Error loading PDF: ${file.name}`, error);
throw error;
}
}
}
return decryptedFiles;
}
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
const startTime = performance.now();
const file = formData.get('fileInput');
@@ -140,7 +242,7 @@
let errorMessage = null;
try {
const response = await fetch(url, {method: 'POST', body: formData});
const response = await window.fetchWithCsrf(url, {method: 'POST', body: formData});
const contentType = response.headers.get('content-type');
if (!response.ok) {
@@ -163,10 +265,10 @@
success = true;
if (contentType.includes('application/pdf') || contentType.includes('image/')) {
clearFileInput();
//clearFileInput();
return handleResponse(blob, filename, !isMulti, isZip);
} else {
clearFileInput();
//clearFileInput();
return handleResponse(blob, filename, false, isZip);
}
} catch (error) {

View File

@@ -1,6 +1,6 @@
const DraggableUtils = {
boxDragContainer: document.getElementById("box-drag-container"),
pdfCanvas: document.getElementById("pdf-canvas"),
boxDragContainer: document.getElementById('box-drag-container'),
pdfCanvas: document.getElementById('pdf-canvas'),
nextId: 0,
pdfDoc: null,
pageIndex: 0,
@@ -9,19 +9,17 @@ const DraggableUtils = {
lastInteracted: null,
init() {
interact(".draggable-canvas")
interact('.draggable-canvas')
.draggable({
listeners: {
move: (event) => {
const target = event.target;
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
+ event.dx;
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
+ event.dy;
const x = (parseFloat(target.getAttribute('data-bs-x')) || 0) + event.dx;
const y = (parseFloat(target.getAttribute('data-bs-y')) || 0) + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.setAttribute('data-bs-x', x);
target.setAttribute('data-bs-y', y);
this.onInteraction(target);
//update the last interacted element
@@ -30,12 +28,12 @@ const DraggableUtils = {
},
})
.resizable({
edges: { left: true, right: true, bottom: true, top: true },
edges: {left: true, right: true, bottom: true, top: true},
listeners: {
move: (event) => {
var target = event.target;
var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
var x = parseFloat(target.getAttribute('data-bs-x')) || 0;
var y = parseFloat(target.getAttribute('data-bs-y')) || 0;
// check if control key is pressed
if (event.ctrlKey) {
@@ -44,8 +42,7 @@ const DraggableUtils = {
let width = event.rect.width;
let height = event.rect.height;
if (Math.abs(event.deltaRect.width) >= Math.abs(
event.deltaRect.height)) {
if (Math.abs(event.deltaRect.width) >= Math.abs(event.deltaRect.height)) {
height = width / aspectRatio;
} else {
width = height * aspectRatio;
@@ -55,19 +52,18 @@ const DraggableUtils = {
event.rect.height = height;
}
target.style.width = event.rect.width + "px";
target.style.height = event.rect.height + "px";
target.style.width = event.rect.width + 'px';
target.style.height = event.rect.height + 'px';
// translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.style.transform = "translate(" + x + "px," + y + "px)";
target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + "\u00D7"
+ Math.round(event.rect.height);
target.setAttribute('data-bs-x', x);
target.setAttribute('data-bs-y', y);
target.textContent = Math.round(event.rect.width) + '\u00D7' + Math.round(event.rect.height);
this.onInteraction(target);
},
@@ -75,7 +71,7 @@ const DraggableUtils = {
modifiers: [
interact.modifiers.restrictSize({
min: { width: 5, height: 5 },
min: {width: 5, height: 5},
}),
],
inertia: true,
@@ -95,8 +91,8 @@ const DraggableUtils = {
const stepY = target.offsetHeight * 0.05;
// Get the current x and y coordinates
let x = (parseFloat(target.getAttribute('data-bs-x')) || 0);
let y = (parseFloat(target.getAttribute('data-bs-y')) || 0);
let x = parseFloat(target.getAttribute('data-bs-x')) || 0;
let y = parseFloat(target.getAttribute('data-bs-y')) || 0;
// Check which key was pressed and update the coordinates accordingly
switch (event.key) {
@@ -135,15 +131,15 @@ const DraggableUtils = {
},
createDraggableCanvas() {
const createdCanvas = document.createElement("canvas");
const createdCanvas = document.createElement('canvas');
createdCanvas.id = `draggable-canvas-${this.nextId++}`;
createdCanvas.classList.add("draggable-canvas");
createdCanvas.classList.add('draggable-canvas');
const x = 0;
const y = 20;
createdCanvas.style.transform = `translate(${x}px, ${y}px)`;
createdCanvas.setAttribute("data-bs-x", x);
createdCanvas.setAttribute("data-bs-y", y);
createdCanvas.setAttribute('data-bs-x', x);
createdCanvas.setAttribute('data-bs-y', y);
//Click element in order to enable arrow keys
createdCanvas.addEventListener('click', () => {
@@ -186,29 +182,29 @@ const DraggableUtils = {
newHeight = newHeight * scaleMultiplier;
}
createdCanvas.style.width = newWidth + "px";
createdCanvas.style.height = newHeight + "px";
createdCanvas.style.width = newWidth + 'px';
createdCanvas.style.height = newHeight + 'px';
var myContext = createdCanvas.getContext("2d");
var myContext = createdCanvas.getContext('2d');
myContext.drawImage(myImage, 0, 0);
resolve(createdCanvas);
};
});
},
deleteAllDraggableCanvases() {
this.boxDragContainer.querySelectorAll(".draggable-canvas").forEach((el) => el.remove());
this.boxDragContainer.querySelectorAll('.draggable-canvas').forEach((el) => el.remove());
},
async addAllPagesDraggableCanvas(element) {
if (element) {
let currentPage = this.pageIndex
let currentPage = this.pageIndex;
if (!this.elementAllPages.includes(element)) {
this.elementAllPages.push(element)
this.elementAllPages.push(element);
element.style.filter = 'sepia(1) hue-rotate(90deg) brightness(1.2)';
let newElement = {
"element": element,
"offsetWidth": element.width,
"offsetHeight": element.height
}
element: element,
offsetWidth: element.width,
offsetHeight: element.height,
};
let pagesMap = this.documentsMap.get(this.pdfDoc);
@@ -216,21 +212,20 @@ const DraggableUtils = {
pagesMap = {};
this.documentsMap.set(this.pdfDoc, pagesMap);
}
let page = this.pageIndex
let page = this.pageIndex;
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
if (pagesMap[`${pageIndex}-offsetWidth`]) {
if (!pagesMap[pageIndex].includes(newElement)) {
pagesMap[pageIndex].push(newElement);
}
} else {
pagesMap[pageIndex] = []
pagesMap[pageIndex].push(newElement)
pagesMap[pageIndex] = [];
pagesMap[pageIndex].push(newElement);
pagesMap[`${pageIndex}-offsetWidth`] = pagesMap[`${page}-offsetWidth`];
pagesMap[`${pageIndex}-offsetHeight`] = pagesMap[`${page}-offsetHeight`];
}
await this.goToPage(pageIndex)
await this.goToPage(pageIndex);
}
} else {
const index = this.elementAllPages.indexOf(element);
@@ -247,17 +242,17 @@ const DraggableUtils = {
for (let pageIndex = 0; pageIndex < this.pdfDoc.numPages; pageIndex++) {
if (pagesMap[`${pageIndex}-offsetWidth`] && pageIndex != currentPage) {
const pageElements = pagesMap[pageIndex];
pageElements.forEach(elementPage => {
const elementIndex = pageElements.findIndex(elementPage => elementPage['element'].id === element.id);
pageElements.forEach((elementPage) => {
const elementIndex = pageElements.findIndex((elementPage) => elementPage['element'].id === element.id);
if (elementIndex !== -1) {
pageElements.splice(elementIndex, 1);
}
});
}
await this.goToPage(pageIndex)
await this.goToPage(pageIndex);
}
}
await this.goToPage(currentPage)
await this.goToPage(currentPage);
}
},
deleteDraggableCanvas(element) {
@@ -271,7 +266,7 @@ const DraggableUtils = {
}
},
getLastInteracted() {
return this.boxDragContainer.querySelector(".draggable-canvas:last-of-type");
return this.boxDragContainer.querySelector('.draggable-canvas:last-of-type');
},
storePageContents() {
@@ -280,7 +275,7 @@ const DraggableUtils = {
pagesMap = {};
}
const elements = [...this.boxDragContainer.querySelectorAll(".draggable-canvas")];
const elements = [...this.boxDragContainer.querySelectorAll('.draggable-canvas')];
const draggablesData = elements.map((el) => {
return {
element: el,
@@ -291,8 +286,8 @@ const DraggableUtils = {
elements.forEach((el) => this.boxDragContainer.removeChild(el));
pagesMap[this.pageIndex] = draggablesData;
pagesMap[this.pageIndex + "-offsetWidth"] = this.pdfCanvas.offsetWidth;
pagesMap[this.pageIndex + "-offsetHeight"] = this.pdfCanvas.offsetHeight;
pagesMap[this.pageIndex + '-offsetWidth'] = this.pdfCanvas.offsetWidth;
pagesMap[this.pageIndex + '-offsetHeight'] = this.pdfCanvas.offsetHeight;
this.documentsMap.set(this.pdfDoc, pagesMap);
},
@@ -329,8 +324,8 @@ const DraggableUtils = {
// render the page onto the canvas
var renderContext = {
canvasContext: this.pdfCanvas.getContext("2d"),
viewport: page.getViewport({ scale: 1 }),
canvasContext: this.pdfCanvas.getContext('2d'),
viewport: page.getViewport({scale: 1}),
};
await page.render(renderContext).promise;
@@ -358,7 +353,7 @@ const DraggableUtils = {
}
},
parseTransform(element) { },
parseTransform(element) {},
async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
@@ -369,7 +364,7 @@ const DraggableUtils = {
const pagesMap = this.documentsMap.get(this.pdfDoc);
for (let pageIdx in pagesMap) {
if (pageIdx.includes("offset")) {
if (pageIdx.includes('offset')) {
continue;
}
console.log(typeof pageIdx);
@@ -377,9 +372,8 @@ const DraggableUtils = {
const page = pdfDocModified.getPage(parseInt(pageIdx));
let draggablesData = pagesMap[pageIdx];
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
const offsetWidth = pagesMap[pageIdx + '-offsetWidth'];
const offsetHeight = pagesMap[pageIdx + '-offsetHeight'];
for (const draggableData of draggablesData) {
// embed the draggable canvas
@@ -389,8 +383,8 @@ const DraggableUtils = {
const pdfImageObject = await pdfDocModified.embedPng(draggableImgBytes);
// calculate the position in the pdf document
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, "");
const transformComponents = tansform.split(",");
const tansform = draggableElement.style.transform.replace(/[^.,-\d]/g, '');
const transformComponents = tansform.split(',');
const draggablePositionPixels = {
x: parseFloat(transformComponents[0]),
y: parseFloat(transformComponents[1]),
@@ -429,9 +423,8 @@ const DraggableUtils = {
};
//Defining the image if the page has a 0-degree angle
let x = draggablePositionPdf.x
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
let x = draggablePositionPdf.x;
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
//Defining the image position if it is at other angles
if (normalizedAngle === 90) {
@@ -451,7 +444,7 @@ const DraggableUtils = {
y: y,
width: draggablePositionPdf.width,
height: draggablePositionPdf.height,
rotate: rotation
rotate: rotation,
});
}
}
@@ -460,6 +453,6 @@ const DraggableUtils = {
},
};
document.addEventListener("DOMContentLoaded", () => {
document.addEventListener('DOMContentLoaded', () => {
DraggableUtils.init();
});

View File

@@ -8,7 +8,6 @@ window.fetchWithCsrf = async function(url, options = {}) {
if (cookieValue) {
return cookieValue;
}
const csrfElement = document.querySelector('input[name="_csrf"]');
return csrfElement ? csrfElement.value : null;
}

View File

@@ -1,20 +1,19 @@
import FileIconFactory from "./file-icon-factory.js";
import FileUtils from "./file-utils.js";
import FileIconFactory from './file-icon-factory.js';
import FileUtils from './file-utils.js';
import UUID from './uuid.js';
import {DecryptFile} from './DecryptFiles.js';
let isScriptExecuted = false;
if (!isScriptExecuted) {
isScriptExecuted = true;
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput);
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('.custom-file-chooser').forEach(setupFileInput);
});
}
function setupFileInput(chooser) {
const elementId = chooser.getAttribute("data-bs-element-id");
const filesSelected = chooser.getAttribute("data-bs-files-selected");
const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt");
const elementId = chooser.getAttribute('data-bs-element-id');
const filesSelected = chooser.getAttribute('data-bs-files-selected');
const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt');
const inputContainerId = chooser.getAttribute('data-bs-element-container-id');
let inputContainer = document.getElementById(inputContainerId);
@@ -26,7 +25,7 @@ function setupFileInput(chooser) {
inputContainer.addEventListener('click', (e) => {
let inputBtn = document.getElementById(elementId);
inputBtn.click();
})
});
const dragenterListener = function () {
dragCounter++;
@@ -63,7 +62,7 @@ function setupFileInput(chooser) {
const files = dt.files;
const fileInput = document.getElementById(elementId);
if (fileInput?.hasAttribute("multiple")) {
if (fileInput?.hasAttribute('multiple')) {
pushFileListTo(files, allFiles);
} else if (fileInput) {
allFiles = [files[0]];
@@ -78,7 +77,7 @@ function setupFileInput(chooser) {
dragCounter = 0;
fileInput.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: {source: 'drag-drop'} }));
fileInput.dispatchEvent(new CustomEvent('change', {bubbles: true, detail: {source: 'drag-drop'}}));
};
function pushFileListTo(fileList, container) {
@@ -87,7 +86,7 @@ function setupFileInput(chooser) {
}
}
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false);
});
@@ -96,37 +95,50 @@ function setupFileInput(chooser) {
e.stopPropagation();
}
document.body.addEventListener("dragenter", dragenterListener);
document.body.addEventListener("dragleave", dragleaveListener);
document.body.addEventListener("drop", dropListener);
document.body.addEventListener('dragenter', dragenterListener);
document.body.addEventListener('dragleave', dragleaveListener);
document.body.addEventListener('drop', dropListener);
$("#" + elementId).on("change", function (e) {
$('#' + elementId).on('change', async function (e) {
let element = e.target;
const isDragAndDrop = e.detail?.source == 'drag-drop';
if (element instanceof HTMLInputElement && element.hasAttribute("multiple")) {
allFiles = isDragAndDrop ? allFiles : [... allFiles, ... element.files];
if (element instanceof HTMLInputElement && element.hasAttribute('multiple')) {
allFiles = isDragAndDrop ? allFiles : [...allFiles, ...element.files];
} else {
allFiles = Array.from(isDragAndDrop ? allFiles : [element.files[0]]);
}
allFiles = allFiles.map(file => {
if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
return file;
});
allFiles = await Promise.all(
allFiles.map(async (file) => {
let decryptedFile = file;
try {
const decryptFile = new DecryptFile();
const {isEncrypted, requiresPassword} = await decryptFile.checkFileEncrypted(file);
if (file.type === 'application/pdf' && isEncrypted) {
decryptedFile = await decryptFile.decryptFile(file, requiresPassword);
if (!decryptedFile) throw new Error('File decryption failed.');
}
decryptedFile.uniqueId = UUID.uuidv4();
return decryptedFile;
} catch (error) {
console.error(`Error decrypting file: ${file.name}`, error);
if (!file.uniqueId) file.uniqueId = UUID.uuidv4();
return file;
}
})
);
if (!isDragAndDrop) {
let dataTransfer = toDataTransfer(allFiles);
element.files = dataTransfer.files;
let dataTransfer = toDataTransfer(allFiles);
element.files = dataTransfer.files;
}
handleFileInputChange(this);
this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
});
this.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true, detail: {elementId, allFiles}}));
});
function toDataTransfer(files) {
let dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
files.forEach((file) => dataTransfer.items.add(file));
return dataTransfer;
}
@@ -136,7 +148,7 @@ function setupFileInput(chooser) {
const filesInfo = files.map((f) => ({name: f.name, size: f.size, uniqueId: f.uniqueId}));
const selectedFilesContainer = $(inputContainer).siblings(".selected-files");
const selectedFilesContainer = $(inputContainer).siblings('.selected-files');
selectedFilesContainer.empty();
filesInfo.forEach((info) => {
let fileContainerClasses = 'small-file-container d-flex flex-column justify-content-center align-items-center';
@@ -167,28 +179,26 @@ function setupFileInput(chooser) {
}
function showOrHideSelectedFilesContainer(files) {
if (files && files.length > 0)
chooser.style.setProperty('--selected-files-display', 'flex');
else
chooser.style.setProperty('--selected-files-display', 'none');
if (files && files.length > 0) chooser.style.setProperty('--selected-files-display', 'flex');
else chooser.style.setProperty('--selected-files-display', 'none');
}
function removeFileListener(e) {
const fileId = (e.target).getAttribute('data-file-id');
const fileId = e.target.getAttribute('data-file-id');
let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);
showOrHideSelectedFilesContainer(allFiles);
inputElement.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
inputElement.dispatchEvent(new CustomEvent('file-input-change', {bubbles: true}));
}
function removeFileById(fileId, inputElement) {
let fileContainer = document.getElementById(fileId);
fileContainer.remove();
allFiles = allFiles.filter(v => v.uniqueId != fileId);
allFiles = allFiles.filter((v) => v.uniqueId != fileId);
let dataTransfer = toDataTransfer(allFiles);
if (inputElement) inputElement.files = dataTransfer.files;
@@ -207,23 +217,19 @@ function setupFileInput(chooser) {
}
function createFileInfoContainer(info) {
let fileInfoContainer = document.createElement("div");
let fileInfoContainer = document.createElement('div');
let fileInfoContainerClasses = 'file-info d-flex flex-column align-items-center justify-content-center';
$(fileInfoContainer).addClass(fileInfoContainerClasses);
$(fileInfoContainer).append(
`<div title="${info.name}">${info.name}</div>`
);
$(fileInfoContainer).append(`<div title="${info.name}">${info.name}</div>`);
let fileSizeWithUnits = FileUtils.transformFileSize(info.size);
$(fileInfoContainer).append(
`<div title="${info.size}">${fileSizeWithUnits}</div>`
);
$(fileInfoContainer).append(`<div title="${info.size}">${fileSizeWithUnits}</div>`);
return fileInfoContainer;
}
//Listen for event of file being removed and the filter it out of the allFiles array
document.addEventListener("fileRemoved", function (e) {
document.addEventListener('fileRemoved', function (e) {
const fileId = e.detail;
let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);

View File

@@ -268,7 +268,7 @@ document.addEventListener("DOMContentLoaded", function () {
const parent = header.parentNode;
const container = header.parentNode.querySelector(".feature-group-container");
if (parent.id !== "groupFavorites") {
container.style.maxHeight = container.clientHeight + "px";
container.style.maxHeight = container.scrollHeight + "px";
}
header.onclick = () => {
expandCollapseToggle(parent);

View File

@@ -5,6 +5,7 @@ import {SplitAllCommand} from './commands/split.js';
import {UndoManager} from './UndoManager.js';
import {PageBreakCommand} from './commands/page-break.js';
import {AddFilesCommand} from './commands/add-page.js';
import {DecryptFile} from '../DecryptFiles.js';
class PdfContainer {
fileName;
@@ -40,6 +41,8 @@ class PdfContainer {
this.removeAllElements = this.removeAllElements.bind(this);
this.resetPages = this.resetPages.bind(this);
this.decryptFile = new DecryptFile();
this.undoManager = undoManager || new UndoManager();
this.pdfAdapters = pdfAdapters;
@@ -165,7 +168,6 @@ class PdfContainer {
input.click();
});
}
async addFilesFromFiles(files, nextSiblingElement, pages) {
this.fileName = files[0].name;
for (var i = 0; i < files.length; i++) {
@@ -173,17 +175,37 @@ class PdfContainer {
let processingTime,
errorMessage = null,
pageCount = 0;
try {
const file = files[i];
if (file.type === 'application/pdf') {
const {renderer, pdfDocument} = await this.loadFile(file);
let decryptedFile = files[i];
let isEncrypted = false;
let requiresPassword = false;
await this.decryptFile
.checkFileEncrypted(decryptedFile)
.then((result) => {
isEncrypted = result.isEncrypted;
requiresPassword = result.requiresPassword;
})
.catch((error) => {
console.error(error);
});
if (decryptedFile.type === 'application/pdf' && isEncrypted) {
decryptedFile = await this.decryptFile.decryptFile(decryptedFile, requiresPassword);
if (!decryptedFile) {
throw new Error('File decryption failed.');
}
}
if (decryptedFile.type === 'application/pdf') {
const {renderer, pdfDocument} = await this.loadFile(decryptedFile);
pageCount = renderer.pageCount || 0;
pages = await this.addPdfFile(renderer, pdfDocument, nextSiblingElement, pages);
} else if (file.type.startsWith('image/')) {
pages = await this.addImageFile(file, nextSiblingElement, pages);
} else if (decryptedFile.type.startsWith('image/')) {
pages = await this.addImageFile(decryptedFile, nextSiblingElement, pages);
}
processingTime = Date.now() - startTime;
this.captureFileProcessingEvent(true, file, processingTime, null, pageCount);
this.captureFileProcessingEvent(true, decryptedFile, processingTime, null, pageCount);
} catch (error) {
processingTime = Date.now() - startTime;
errorMessage = error.message || 'Unknown error';
@@ -194,6 +216,7 @@ class PdfContainer {
document.querySelectorAll('.enable-on-file').forEach((element) => {
element.disabled = false;
});
return pages;
}

View File

@@ -0,0 +1,47 @@
document.getElementById('download-pdf').addEventListener('click', async () => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_addedImage.pdf';
link.click();
});
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const fileInput = event.target;
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, '');
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({data: pdfData}).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = '';
});
}
});
});
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = 'display:none !important';
});
});
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', (e) => {
if (!e.target.files) {
return;
}
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});

View File

@@ -0,0 +1,253 @@
var canvas = document.getElementById('contrast-pdf-canvas');
var context = canvas.getContext('2d');
var originalImageData = null;
var allPages = [];
var pdfDoc = null;
var pdf = null; // This is the current PDF document
async function renderPDFAndSaveOriginalImageData(file) {
var fileReader = new FileReader();
fileReader.onload = async function () {
var data = new Uint8Array(this.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdf = await pdfjsLib.getDocument({data: data}).promise;
// Get the number of pages in the PDF
var numPages = pdf.numPages;
allPages = Array.from({length: numPages}, (_, i) => i + 1);
// Create a new PDF document
pdfDoc = await PDFLib.PDFDocument.create();
// Render the first page in the viewer
await renderPageAndAdjustImageProperties(1);
document.getElementById('sliders-container').style.display = 'block';
};
fileReader.readAsArrayBuffer(file);
}
// This function is now async and returns a promise
function renderPageAndAdjustImageProperties(pageNum) {
return new Promise(async function (resolve, reject) {
var page = await pdf.getPage(pageNum);
var scale = 1.5;
var viewport = page.getViewport({scale: scale});
canvas.height = viewport.height;
canvas.width = viewport.width;
var renderContext = {
canvasContext: context,
viewport: viewport,
};
var renderTask = page.render(renderContext);
renderTask.promise.then(function () {
originalImageData = context.getImageData(0, 0, canvas.width, canvas.height);
adjustImageProperties();
resolve();
});
canvas.classList.add('fixed-shadow-canvas');
});
}
function adjustImageProperties() {
var contrast = parseFloat(document.getElementById('contrast-slider').value);
var brightness = parseFloat(document.getElementById('brightness-slider').value);
var saturation = parseFloat(document.getElementById('saturation-slider').value);
contrast /= 100; // normalize to range [0, 2]
brightness /= 100; // normalize to range [0, 2]
saturation /= 100; // normalize to range [0, 2]
if (originalImageData) {
var newImageData = context.createImageData(originalImageData.width, originalImageData.height);
newImageData.data.set(originalImageData.data);
for (var i = 0; i < newImageData.data.length; i += 4) {
var r = newImageData.data[i];
var g = newImageData.data[i + 1];
var b = newImageData.data[i + 2];
// Adjust contrast
r = adjustContrastForPixel(r, contrast);
g = adjustContrastForPixel(g, contrast);
b = adjustContrastForPixel(b, contrast);
// Adjust brightness
r = adjustBrightnessForPixel(r, brightness);
g = adjustBrightnessForPixel(g, brightness);
b = adjustBrightnessForPixel(b, brightness);
// Adjust saturation
var rgb = adjustSaturationForPixel(r, g, b, saturation);
newImageData.data[i] = rgb[0];
newImageData.data[i + 1] = rgb[1];
newImageData.data[i + 2] = rgb[2];
}
context.putImageData(newImageData, 0, 0);
}
}
function rgbToHsl(r, g, b) {
(r /= 255), (g /= 255), (b /= 255);
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
function hslToRgb(h, s, l) {
var r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
var hue2rgb = function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
function adjustContrastForPixel(pixel, contrast) {
// Normalize to range [-0.5, 0.5]
var normalized = pixel / 255 - 0.5;
// Apply contrast
normalized *= contrast;
// Denormalize back to [0, 255]
return (normalized + 0.5) * 255;
}
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
function adjustSaturationForPixel(r, g, b, saturation) {
var hsl = rgbToHsl(r, g, b);
// Adjust saturation
hsl[1] = clamp(hsl[1] * saturation, 0, 1);
// Convert back to RGB
var rgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
// Return adjusted RGB values
return rgb;
}
function adjustBrightnessForPixel(pixel, brightness) {
return Math.max(0, Math.min(255, pixel * brightness));
}
let inputFileName = '';
async function downloadPDF() {
for (var i = 0; i < allPages.length; i++) {
await renderPageAndAdjustImageProperties(allPages[i]);
const pngImageBytes = canvas.toDataURL('image/png');
const pngImage = await pdfDoc.embedPng(pngImageBytes);
const pngDims = pngImage.scale(1);
// Create a blank page matching the dimensions of the image
const page = pdfDoc.addPage([pngDims.width, pngDims.height]);
// Draw the PNG image
page.drawImage(pngImage, {
x: 0,
y: 0,
width: pngDims.width,
height: pngDims.height,
});
}
// Serialize the PDFDocument to bytes (a Uint8Array)
const pdfBytes = await pdfDoc.save();
// Create a Blob
const blob = new Blob([pdfBytes.buffer], {type: 'application/pdf'});
// Create download link
const downloadLink = document.createElement('a');
downloadLink.href = URL.createObjectURL(blob);
let newFileName = inputFileName ? inputFileName.replace('.pdf', '') : 'download';
newFileName += '_adjusted_color.pdf';
downloadLink.download = newFileName;
downloadLink.click();
// After download, reset the viewer and clear stored data
allPages = []; // Clear the pages
originalImageData = null; // Clear the image data
// Go back to page 1 and render it in the viewer
if (pdf !== null) {
renderPageAndAdjustImageProperties(1);
}
}
// Event listeners
document.getElementById('fileInput-input').addEventListener('change', function (e) {
const fileInput = event.target;
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
inputFileName = file.name;
renderPDFAndSaveOriginalImageData(file);
}
});
});
document.getElementById('contrast-slider').addEventListener('input', function () {
document.getElementById('contrast-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('brightness-slider').addEventListener('input', function () {
document.getElementById('brightness-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('saturation-slider').addEventListener('input', function () {
document.getElementById('saturation-val').textContent = this.value;
adjustImageProperties();
});
document.getElementById('download-button').addEventListener('click', function () {
downloadPDF();
});

View File

@@ -0,0 +1,150 @@
const deleteAllCheckbox = document.querySelector('#deleteAll');
let inputs = document.querySelectorAll('input');
const customMetadataDiv = document.getElementById('customMetadata');
const otherMetadataEntriesDiv = document.getElementById('otherMetadataEntries');
deleteAllCheckbox.addEventListener('change', function (event) {
inputs.forEach((input) => {
// If it's the deleteAllCheckbox or any file input, skip
if (input === deleteAllCheckbox || input.type === 'file') {
return;
}
// Disable or enable based on the checkbox state
input.disabled = deleteAllCheckbox.checked;
});
});
const customModeCheckbox = document.getElementById('customModeCheckbox');
const addMetadataBtn = document.getElementById('addMetadataBtn');
const customMetadataFormContainer = document.getElementById('customMetadataEntries');
var count = 1;
const fileInput = document.querySelector('#fileInput-input');
const authorInput = document.querySelector('#author');
const creationDateInput = document.querySelector('#creationDate');
const creatorInput = document.querySelector('#creator');
const keywordsInput = document.querySelector('#keywords');
const modificationDateInput = document.querySelector('#modificationDate');
const producerInput = document.querySelector('#producer');
const subjectInput = document.querySelector('#subject');
const titleInput = document.querySelector('#title');
const trappedInput = document.querySelector('#trapped');
var lastPDFFileMeta = null;
var lastPDFFile = null;
fileInput.addEventListener('change', async function () {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
while (customMetadataFormContainer.firstChild) {
customMetadataFormContainer.removeChild(customMetadataFormContainer.firstChild);
}
var url = URL.createObjectURL(file);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdf = await pdfjsLib.getDocument(url).promise;
const pdfMetadata = await pdf.getMetadata();
lastPDFFile = pdfMetadata?.info;
console.log(pdfMetadata);
if (!pdfMetadata?.info?.Custom || pdfMetadata?.info?.Custom.size == 0) {
customModeCheckbox.disabled = true;
customModeCheckbox.checked = false;
} else {
customModeCheckbox.disabled = false;
}
authorInput.value = pdfMetadata?.info?.Author;
creationDateInput.value = convertDateFormat(pdfMetadata?.info?.CreationDate);
creatorInput.value = pdfMetadata?.info?.Creator;
keywordsInput.value = pdfMetadata?.info?.Keywords;
modificationDateInput.value = convertDateFormat(pdfMetadata?.info?.ModDate);
producerInput.value = pdfMetadata?.info?.Producer;
subjectInput.value = pdfMetadata?.info?.Subject;
titleInput.value = pdfMetadata?.info?.Title;
console.log(pdfMetadata?.info);
const trappedValue = pdfMetadata?.info?.Trapped;
// Get all options in the select element
const options = trappedInput.options;
// Loop through all options to find the one with a matching value
for (let i = 0; i < options.length; i++) {
if (options[i].value === trappedValue) {
options[i].selected = true;
break;
}
}
addExtra();
}
});
});
addMetadataBtn.addEventListener('click', () => {
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.placeholder = 'Key';
keyInput.className = 'form-control';
keyInput.name = `allRequestParams[customKey${count}]`;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value';
valueInput.className = 'form-control';
valueInput.name = `allRequestParams[customValue${count}]`;
count = count + 1;
const formGroup = document.createElement('div');
formGroup.className = 'mb-3';
formGroup.appendChild(keyInput);
formGroup.appendChild(valueInput);
customMetadataFormContainer.appendChild(formGroup);
});
function convertDateFormat(dateTimeString) {
if (!dateTimeString || dateTimeString.length < 17) {
return dateTimeString;
}
const year = dateTimeString.substring(2, 6);
const month = dateTimeString.substring(6, 8);
const day = dateTimeString.substring(8, 10);
const hour = dateTimeString.substring(10, 12);
const minute = dateTimeString.substring(12, 14);
const second = dateTimeString.substring(14, 16);
return year + '/' + month + '/' + day + ' ' + hour + ':' + minute + ':' + second;
}
function addExtra() {
const event = document.getElementById('customModeCheckbox');
if (event.checked && lastPDFFile.Custom != null) {
customMetadataDiv.style.display = 'block';
for (const [key, value] of Object.entries(lastPDFFile.Custom)) {
if (
key === 'Author' ||
key === 'CreationDate' ||
key === 'Creator' ||
key === 'Keywords' ||
key === 'ModDate' ||
key === 'Producer' ||
key === 'Subject' ||
key === 'Title' ||
key === 'Trapped'
) {
continue;
}
const entryDiv = document.createElement('div');
entryDiv.className = 'mb-3';
entryDiv.innerHTML = `<div class="mb-3"><label class="form-check-label" for="${key}">${key}:</label><input name="${key}" value="${value}" type="text" class="form-control" id="${key}"></div>`;
otherMetadataEntriesDiv.appendChild(entryDiv);
}
} else {
customMetadataDiv.style.display = 'none';
while (otherMetadataEntriesDiv.firstChild) {
otherMetadataEntriesDiv.removeChild(otherMetadataEntriesDiv.firstChild);
}
}
}
customModeCheckbox.addEventListener('change', (event) => {
addExtra();
});

View File

@@ -0,0 +1,159 @@
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = 'none';
let containerRect = canvasesContainer.getBoundingClientRect();
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let pageScale = 1; // The scale which the pdf page renders
let timeId = null; // timeout id for resizing canvases event
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
window.addEventListener('resize', function () {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function (e) {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
canvasesContainer.style.display = 'block'; // set for visual purposes
let file = allFiles[0];
renderPageFromFile(file);
}
});
});
cropForm.addEventListener('submit', function (e) {
if (xInput.value == '' && yInput.value == '' && widthInput.value == '' && heightInput.value == '') {
// Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
xInput.value = 0;
yInput.value = 0;
widthInput.value = containerRect.width;
heightInput.value = containerRect.height;
}
});
overlayCanvas.addEventListener('mousedown', function (e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function (e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function (e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX / pageScale;
yInput.value = flippedY / pageScale;
widthInput.value = rectWidth / pageScale;
heightInput.value = rectHeight / pageScale;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function (page) {
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
pageScale = containerRect.width / page.getViewport({scale: 1}).width; // The new scale
let viewport = page.getViewport({scale: containerRect.width / page.getViewport({scale: 1}).width});
canvasesContainer.width = viewport.width;
canvasesContainer.height = viewport.height;
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = {canvasContext: context, viewport: viewport};
page.render(renderContext);
pdfCanvas.classList.add('shadow-canvas');
});
}

View File

@@ -0,0 +1,138 @@
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = 'none';
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let fileInput = document.getElementById('fileInput-input');
let file;
let pdfDoc = null;
let pageId = document.getElementById('pageId');
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let timeId = null; // timeout id for resizing canvases event
btn1Object.addEventListener('click', function (e) {
if (currentPage !== 1) {
currentPage = currentPage - 1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
btn2Object.addEventListener('click', function (e) {
if (currentPage !== totalPages) {
currentPage = currentPage + 1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageId.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById('pagination-button-container').style.display = 'flex';
document.getElementById('instruction-text').style.display = 'block';
}
}
window.addEventListener('resize', function () {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function (e) {
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
canvasesContainer.style.display = 'block'; // set for visual purposes
file = e.target.files[0];
renderPageFromFile(file);
}
});
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function (page) {
let viewport = page.getViewport({scale: 1.0});
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = {canvasContext: context, viewport: viewport};
page.render(renderContext);
pdfCanvas.classList.add('shadow-canvas');
});
}

View File

@@ -0,0 +1,213 @@
window.toggleSignatureView = toggleSignatureView;
window.previewSignature = previewSignature;
window.addSignatureFromPreview = addSignatureFromPreview;
window.addDraggableFromPad = addDraggableFromPad;
window.addDraggableFromText = addDraggableFromText;
window.goToFirstOrLastPage = goToFirstOrLastPage;
let currentPreviewSrc = null;
function toggleSignatureView() {
const gridView = document.getElementById('gridView');
const listView = document.getElementById('listView');
const gridText = document.querySelector('.grid-view-text');
const listText = document.querySelector('.list-view-text');
if (gridView.style.display !== 'none') {
gridView.style.display = 'none';
listView.style.display = 'block';
gridText.style.display = 'none';
listText.style.display = 'inline';
} else {
gridView.style.display = 'block';
listView.style.display = 'none';
gridText.style.display = 'inline';
listText.style.display = 'none';
}
}
function previewSignature(element) {
const src = element.dataset.src;
currentPreviewSrc = src;
const filename = element.querySelector('.signature-list-name').textContent;
const previewImage = document.getElementById('previewImage');
const previewFileName = document.getElementById('previewFileName');
previewImage.src = src;
previewFileName.textContent = filename;
const modal = new bootstrap.Modal(document.getElementById('signaturePreview'));
modal.show();
}
function addSignatureFromPreview() {
if (currentPreviewSrc) {
DraggableUtils.createDraggableCanvasFromUrl(currentPreviewSrc);
bootstrap.Modal.getInstance(document.getElementById('signaturePreview')).hide();
}
}
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const fileInput = event.target;
fileInput.addEventListener('file-input-change', async (e) => {
const {allFiles} = e.detail;
if (allFiles && allFiles.length > 0) {
const file = allFiles[0];
originalFileName = file.name.replace(/\.[^/.]+$/, '');
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({data: pdfData}).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = '';
});
}
});
});
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.show-on-file-selected').forEach((el) => {
el.style.cssText = 'display:none !important';
});
});
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', (e) => {
if (!e.target.files) return;
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: 'black',
});
function addDraggableFromPad() {
if (signaturePad.isEmpty()) return;
const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
}
function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext('2d');
let originalWidth = canvas.width;
let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
let minX = originalWidth + 1,
maxX = -1,
minY = originalHeight + 1,
maxY = -1,
x = 0,
y = 0,
currentPixelColorValueIndex;
for (y = 0; y < originalHeight; y++) {
for (x = 0; x < originalWidth; x++) {
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x;
if (maxX < x) maxX = x;
if (minY > y) minY = y;
if (maxY < y) maxY = y;
}
}
}
let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null;
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
let croppedCanvas = document.createElement('canvas'),
croppedCtx = croppedCanvas.getContext('2d');
croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight;
croppedCtx.putImageData(cuttedImageData, 0, 0);
return croppedCanvas.toDataURL();
}
function resizeCanvas() {
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10;
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas.getContext('2d').scale(ratio * additionalFactor, ratio * additionalFactor);
signaturePad.clear();
}
new IntersectionObserver((entries, observer) => {
if (entries.some((entry) => entry.intersectionRatio > 0)) {
resizeCanvas();
}
}).observe(signaturePadCanvas);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
function addDraggableFromText() {
const sigText = document.getElementById('sigText').value;
const font = document.querySelector('select[name=font]').value;
const fontSize = 100;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize;
let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top';
let y = 0;
paragraphs.forEach((paragraph) => {
ctx.fillText(paragraph, 0, y);
y += fontSize;
});
const dataURL = canvas.toDataURL();
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
}
async function goToFirstOrLastPage(page) {
if (page) {
const lastPage = DraggableUtils.pdfDoc.numPages;
await DraggableUtils.goToPage(lastPage - 1);
} else {
await DraggableUtils.goToPage(0);
}
}
document.getElementById('download-pdf').addEventListener('click', async () => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_signed.pdf';
link.click();
});

View File

@@ -196,7 +196,7 @@
/*<![CDATA[*/
const urlGetApiKey = /*[[@{/api/v1/user/get-api-key}]]*/ "/api/v1/user/get-api-key";
/*]]>*/
let response = await fetch(urlGetApiKey, { method: 'POST' });
let response = await window.fetchWithCsrf(urlGetApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);
@@ -213,7 +213,7 @@
/*<![CDATA[*/
const urlUpdateApiKey = /*[[@{/api/v1/user/update-api-key}]]*/ "/api/v1/user/update-api-key";
/*]]>*/
let response = await fetch(urlUpdateApiKey, { method: 'POST' });
let response = await window.fetchWithCsrf(urlUpdateApiKey, { method: 'POST' });
if (response.status === 200) {
let apiKey = await response.text();
manageUIState(apiKey);

View File

@@ -34,140 +34,8 @@
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = "none";
// let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let fileInput = document.getElementById('fileInput-input');
let file;
let pdfDoc = null;
let pageId = document.getElementById('pageId');
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let timeId = null; // timeout id for resizing canvases event
btn1Object.addEventListener('click',function (e){
if (currentPage !== 1) {
currentPage = currentPage - 1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
btn2Object.addEventListener('click',function (e){
if (currentPage !== totalPages){
currentPage=currentPage+1;
pageId.value = currentPage;
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
});
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function (ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
pageId.value = currentPage;
};
reader.readAsArrayBuffer(file);
document.getElementById("pagination-button-container").style.display = "flex";
document.getElementById("instruction-text").style.display = "block";
}
}
window.addEventListener("resize", function() {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function(e) {
canvasesContainer.style.display = "block"; // set for visual purposes
file = e.target.files[0];
renderPageFromFile(file);
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let viewport = page.getViewport({ scale: 1.0 });
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script type="module" th:src="@{'/js/pages/pdf-to-csv.js'}">
</script>
</div>
</div>

View File

@@ -32,163 +32,7 @@
</div>
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let pdfCanvas = document.getElementById('cropPdfCanvas');
let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = "none";
let containerRect = canvasesContainer.getBoundingClientRect();
let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
overlayCanvas.width = pdfCanvas.width;
overlayCanvas.height = pdfCanvas.height;
let isDrawing = false; // New flag to check if drawing is ongoing
let cropForm = document.getElementById('cropForm');
let fileInput = document.getElementById('fileInput-input');
let xInput = document.getElementById('x');
let yInput = document.getElementById('y');
let widthInput = document.getElementById('width');
let heightInput = document.getElementById('height');
let pdfDoc = null;
let currentPage = 1;
let totalPages = 0;
let startX = 0;
let startY = 0;
let rectWidth = 0;
let rectHeight = 0;
let pageScale = 1; // The scale which the pdf page renders
let timeId = null; // timeout id for resizing canvases event
function renderPageFromFile(file) {
if (file.type === 'application/pdf') {
let reader = new FileReader();
reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf;
totalPages = pdf.numPages;
renderPage(currentPage);
});
};
reader.readAsArrayBuffer(file);
}
}
window.addEventListener("resize", function() {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function(e) {
canvasesContainer.style.display = "block"; // set for visual purposes
let file = e.target.files[0];
renderPageFromFile(file);
});
cropForm.addEventListener('submit', function(e) {
if (xInput.value == "" && yInput.value == "" && widthInput.value == "" && heightInput.value == "") {
// Ορίστε συντεταγμένες για ολόκληρη την επιφάνεια του PDF
xInput.value = 0;
yInput.value = 0;
widthInput.value = containerRect.width;
heightInput.value = containerRect.height;
}
});
overlayCanvas.addEventListener('mousedown', function(e) {
// Clear previously drawn rectangle on the main canvas
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
renderPage(currentPage); // Re-render the PDF
// Clear the overlay canvas to ensure old drawings are removed
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
startX = e.offsetX;
startY = e.offsetY;
isDrawing = true;
});
overlayCanvas.addEventListener('mousemove', function(e) {
if (!isDrawing) return;
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear previous rectangle
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
overlayContext.strokeStyle = 'red';
overlayContext.strokeRect(startX, startY, rectWidth, rectHeight);
});
overlayCanvas.addEventListener('mouseup', function(e) {
isDrawing = false;
rectWidth = e.offsetX - startX;
rectHeight = e.offsetY - startY;
let flippedY = pdfCanvas.height - e.offsetY;
xInput.value = startX / pageScale;
yInput.value = flippedY / pageScale;
widthInput.value = rectWidth / pageScale;
heightInput.value = rectHeight /pageScale;
// Draw the final rectangle on the main canvas
context.strokeStyle = 'red';
context.strokeRect(startX, startY, rectWidth, rectHeight);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height); // Clear the overlay
});
function renderPage(pageNumber) {
pdfDoc.getPage(pageNumber).then(function(page) {
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
pageScale = containerRect.width / page.getViewport({ scale: 1 }).width; // The new scale
let viewport = page.getViewport({ scale: containerRect.width / page.getViewport({ scale: 1 }).width });
canvasesContainer.width =viewport.width;
canvasesContainer.height = viewport.height;
pdfCanvas.width = viewport.width;
pdfCanvas.height = viewport.height;
overlayCanvas.width = viewport.width; // Match overlay canvas size with PDF canvas
overlayCanvas.height = viewport.height;
let renderContext = { canvasContext: context, viewport: viewport };
page.render(renderContext);
pdfCanvas.classList.add("shadow-canvas");
});
}
</script>
<script type="module" th:src="@{'/js/pages/crop.js'}"></script>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>

View File

@@ -19,7 +19,7 @@
<p th:text="#{error.contactTip}"></p>
<div id="button-group">
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
<a href="https://discord.gg/Cn8pWhQRxZ" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
</div>
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
</div>

View File

@@ -24,7 +24,7 @@
<script>
window.stirlingPDF = window.stirlingPDF || {};
</script>
<script th:src="@{'/js/fetch-utils.js'}"></script>
<!-- jQuery -->
<script th:src="@{'/js/thirdParty/jquery.min.js'}"></script>
<script th:src="@{'/js/thirdParty/jquery.validate.min.js'}"></script>
@@ -203,7 +203,17 @@
</script>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:src="@{'/js/downloader.js'}"></script>
<script>
window.decrypt = {
passwordPrompt: '[[#{decrypt.passwordPrompt}]]',
cancelled: '[[#{decrypt.cancelled}]]',
noPassword: '[[#{decrypt.noPassword}]]',
invalidPassword: '[[#{decrypt.invalidPassword}]]',
invalidPasswordHeader: '[[#{decrypt.invalidPasswordHeader}]]',
unexpectedError: '[[#{decrypt.unexpectedError}]]',
serverError: '[[#{decrypt.serverError}]]',
success: '[[#{decrypt.success}]]',
};</script>
<div class="custom-file-chooser mb-3" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
<div class="mb-3 d-flex flex-row justify-content-center align-items-center flex-wrap input-container" th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
<label class="file-input-btn d-none">

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