Compare commits

..

30 Commits

Author SHA1 Message Date
Anthony Stirling
32e46eeb73 Update build.gradle 2024-12-03 10:54:07 +00:00
Omar Ahmed Hassan
b7da84d257 Fix deserialization failure in Change Metadata (#2382)
* Fix deserialization failure from String to Map

Fix deserialization failure from String to Map that caused the following exception:
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public org.springframework.http.ResponseEntity<byte[]> stirling.software.SPDF.controller.api.misc.MetadataController.metadata(stirling.software.SPDF.model.api.misc.MetadataRequest) throws java.io.IOException: [Field error in object 'metadataRequest' on field 'allRequestParams': rejected value [{"customKey1" : "YourCustomKey", "customKeyValue1", "YourCustomValue"}]; codes [typeMismatch.metadataRequest.allRequestParams,typeMismatch.allRequestParams,typeMismatch.java.util.Map,typeMismatch];

* Fix form binding for dynamic Map entries in Change Metadata

- Implemented support for dynamic key-value inputs in Change Metadata form using proper `name` attributes for Map (`allRequestParams`) binding.
- Fix form binding for dynamic Map (`allRequestParams`) entries in Change Metadata as the `allRequestParams` (Map name) was being sent as an empty map.
2024-12-03 08:28:34 +00:00
github-actions[bot]
1c1ead5d62 📝 Update README: Translation Progress Table (#2381)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 19:25:16 +00:00
albanobattistella
6ff53aa5b3 Update messages_it_IT.properties (#2380) 2024-12-02 18:59:05 +00:00
github-actions[bot]
8d60b08cd9 📝 Update README: Translation Progress Table (#2379)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-02 18:32:07 +00:00
github-actions[bot]
64cf5167c0 Update translation files (#2378)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-12-02 18:18:16 +00:00
Omar Ahmed Hassan
de4637e8d4 Fix drag and drop area for file choosers by adding separate ones (#2368)
* Add separate drag and drop area for file choosers

 - Add separate drag and drop area for file choosers

### Why?
Previously, when there were multiple file choosers in the same page, if you attempted to drag and drop any files, they would be added to both file choosers as it was designed at first to handle 1 file chooser present, now that we have multiple ones, it is necessary to adapt our design to match the changing functionality.

### Can you not preserve the old overlay when there's only one file chooser present?
Yes, we can, but imagine as a user, you try to drag and drop a file in one page and the fields turn into drag and drop areas then you go to another page and try to drag and drop again but you encounter the old overlay instead, as a user you might get confused and ask yourself "What changed?" or if a user is telling another user the steps to drag and drop files and he didn't know about this case, then it would still be confusing, thus consistency is preferred in this case.

* Update file chooser UI

* Add support for listing and removing selected files and their file icons

- Selected files are listed below the file chooser in a selected files container.
- Users can now remove uploaded/selected files.
- Hide selected files container/box unless there are files selected/uploaded.
- Add separate overlay for each drag & drop area.

## FAQ:
- Why did you assign a unique id to each file? isn't the filename enough?
= Because a user might upload multiple files with the same name, if the user wanted to remove one of them, how would we differentiate between them? we won't be able to unless we assign an identifier, you might argue "we remove based on the filename and size", then what if the user uploaded the same file more than once (intentionally), then we would accidentally remove all the files even though that is not what the user wanted, so going with unique ID approach would prevent this issue/problem from occurring in the first place.

* Rename remove-file css class to remove-selected-file

- Rename remove-file css class to remove-selected-file to avoid css conflict with remove-file in merge.css

* Use input element to dispatch event on file removal

Use the correct element to dispatch "file-input-change" (input element is the correct one).

* Adapt file chooser UI to themes

- Adapt file chooser UI to themes by adjusting their font colors and background colors.
- Make text more visible in overlay by increasing the font size by 0.1rem and setting font weight to 550.

* Remove extra overlay border

- Removing overlay's border as it is unnecessary and only causing a double border issue on the file input container.

* Remove Browse button, highlight file chooser and make it clickable

- Remove browse button.
- Make the entire file chooser container clickable.
- Add glowing effect on hover for file chooser.
- Change color of file chooser on hover.

* Replace crypto.randomUUID() with UUID.uuidv4()

- Replace crypto.randomUUID() with UUID.uuidv4() as crypto.randomUUID() is only supported in secured contexts such as localhost 127.0.0.1 and over HTTPS

* Fix merge file removal not being reflected in file chooser

- Files removed from the list in merge page would now be reflected in the file chooser's container.

* Make inputElement optional in removeFileById

- Make inputElement optional in removeFileById, this way we could control changing inputElements files.

* Add translation support to file chooser

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-12-02 18:10:12 +00:00
Sai Kumar
3c0a8071dc added support for new line break in stampController (#2370)
added support for new line in stampController

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-12-02 17:48:19 +00:00
Omar Ahmed Hassan
04ccdf6f76 Fix: prevent fileInput.js from adding event listeners more than once (#2365)
Fix fileInput.js adding event listeners more than once

- Fix a bug that caused fileInput.js to add event listeners more than once per HTML file as it's included in fileSelector fragment in fragments/common.html thus it's being loaded N times where N is the number of file selectors / custom file chooser / file input elements per HTML file, which resulted in each event actions being executed N times as well, which was prevalent in drag and drop operations such as dragging and dropping a file called y.png, it would be duplicated N times (as in /sign path).
2024-12-02 17:41:11 +00:00
Omar Ahmed Hassan
db02fba31f Fix translations for watermark spacers (#2369)
Fix translations by adding a space between width/height and spacer and capitalize the first letter
2024-12-02 17:01:19 +00:00
Omar Ahmed Hassan
5b6f649e4e Fix submit button in crop by adding id (#2374)
- Add missing ID to submit button in crop page.
2024-12-02 10:40:46 +00:00
Omar Ahmed Hassan
de23bb702c Fix allowing multiple files to be dropped onto a single file input (#2359)
Fix a bug that allowed multiple files to be dropped onto a single-file input element

- Fix a bug that allowed multiple files to be dropped onto a single-file input element by accepting only the first file.
2024-11-29 17:31:14 +00:00
github-actions[bot]
25e564154e Update 3rd Party Licenses (#2362)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-29 15:41:29 +00:00
Anthony Stirling
3633a979d3 fixes and other changes and debug of WIP SAML (#2360)
* backup

* remove debugs

* oauth to saml and compare fixes etc

* ee flag for saml

* more fixes

* info to debug

* remove unused repo

* spring dev fix for saml

* debugs

* saml stuff

* debugs

* fix
2024-11-29 15:11:59 +00:00
Omar Ahmed Hassan
99d481d69f Fix Array.from syntax in nonmultiple file upload (#2357)
- Fix Array.from syntax in nonmultiple file upload as Array.from(<non-array or string>) returns an empty array which is the case when a file is selected from an input element (when multiple attribute isn't  supported) which can be found in Array.from(element.files[0]) -> results in an empty array.
2024-11-29 12:22:52 +00:00
github-actions[bot]
a5ba6c403a 📝 Update README: Translation Progress Table (#2356)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-28 15:39:39 +00:00
albanobattistella
b2e6d89d16 Update messages_it_IT.properties (#2355) 2024-11-28 14:42:55 +00:00
github-actions[bot]
b59d2d15b4 Update translation files (#2354)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-28 14:42:29 +00:00
Omar Ahmed Hassan
61e750646c Feature: Undo Redo options multi tool #2297 (#2348)
* Implement Command class for Command Pattern

Created a base `Command` class to implement the **Command Pattern**. This class provides a skeletal implementation for `execute`, `undo`, and `redo` methods.

**Note:** This class is intended to be subclassed and not instantiated directly.

* Add undo/redo stacks and operations

* Use rotate element command to perform execute/undo/redo operations

* Handle commands executed through events
- Add "command-execution" event listener to execute commands that are not invoked from the same class while adding the command to the undo stack and clearing the redo stack.

* Add and use rotate all command to rotate/redo/undo all elements

* Use command pattern to delete pages

* Use command pattern for page selection

* Use command pattern to move pages up and down

* Use command pattern to remove selected pages

* Use command pattern to perform the splitting operation

* Add undo/redo functionality with filename input exclusion

- Implement undo (Ctrl+Z) and redo (Ctrl+Y) functionality.
- Prevent undo/redo actions when the filename input field is focused.
- Ensures proper handling of undo/redo actions without interfering with text editing.

* Introduce UndoManager for managing undo/redo operations

 - Encapsulate undo/redo stacks and operations within UndoManager.
- Simplify handling of undo/redo functionality through a dedicated manager.

* Call execute on splitAllCommand

- Fix a bug that caused split all functionality to not work as execute() wasn't called on splitAllCommand

* Add undo/redo buttons to multi tool

- Add undo/redo buttons to multi tool
- Dispatch an event upon state change (such as changes in the undo/redo stacks) to update the UI accordingly.

* Add undo/redo to translations

* Replace hard-coded "Undo"/"Redo" with translation keys in multi tool

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-28 14:25:13 +00:00
Omar Ahmed Hassan
de9c21b3de Fix: page break insertion functionality in Multi Tool (#2350)
Fix page break insertion functionality

- Page Break insertion functionality now successfully inserts page breaks upon request
2024-11-28 10:21:14 +00:00
Nureddin Farzaliyev
b32d6cb858 Azerbaijani Language Translation (#2347)
* Azerbaijani flag and dropdown item added

* Azerbaijani Language file Added

* AZ - ignore_translation.toml init

* AZ Translation Enterprise Edition Section

* Translation for Generic

* translation-az pipeline

* Translation for Analytics

* Translation for NAVBAR

* Translation for SETTINGS

* translation-az homepage

* Translation for #login

* Translation for (showJS)

* Translation for #showJS

* Translation for #PDFToWord

* Translation for #PDFToPresentation

* Translation for #PDFToText

* Translation for #PDFToHTML

* Translation for #PDFToXML

* Translation for #PDFToCSV

* Translation for #repair

* Translation for #pageLayout

* Translation for #pdfToSinglePage

* Translation for #pageExtracter

* Translation for #getPdfInfo

* Translation for #markdown-to-pdf

* Translation for #PDFToXML

* Translation for #html-to-pdf

* Translation for #PDFToHTML

* Translation for #PDFToText

* Translation for #PDFToPresentation

* Translation for #PDFToWord

* Translation for #PDFToCSV

* Translation for #url-to-pdf

* Translation for #pdfToImage

* Translation for #BookToPDF

* Translation for #PDFToBook

* Translation for #autoRedact

* Translation for #Add image

* Translation for #File to PDF

* Translation for (remove-image)

* Translation for (remove-image)

* Translation for (survey)

* Translation for (licenses)

* Translation for (printFile)

* Translation for (split-bysections)

* Translation for (overlay-pdfs)

* Translation for (split-by-size-or-count)

* Translation for (addPageNumbers)

* Translation for (adjustContrast)

* Translation for (autoSplitPDF)

* Translation for (scalePages)

* Translation for (removeCertSign)

* Translation for (removeAnnotations)

* Translation for (sign)

* Translation for (flatten)

* Translation for (extractImages)

* Translation for (merge)

* Translation for (view pdf)

* Translation for (pageRemover)

* Translation for (rotate)

* az Translation for replace-invert-color

* az Translation for addstamprequest

* Translation for (remove-image)

* Translation for #url-to-pdf

* Update messages_az_AZ.properties

* Update README.md

* Update README.md

* Update messages_az_AZ.properties

* Translation for #compress

* Translation for #Change permissions

* Translation for #remove password

* Translation for #pdfToPDFA

* Translation for #changeMetadata

* Translation for #merge

* Translation for #split-pdfs

* translation-az addpass

* translation-az watermark

* translation-az removeblanks

* translation-az compare

* translation-az certsign

* Translation for #pdfOrganiser

* Translation for #multiTool

* Translation for #multiTool

* az translation scannerimagesplit

* az translation ocr

* az translation fix

* az translation linebreak fix

* az translation linebreak fix

---------

Co-authored-by: Lucifer25x <lucifer25x@protonmail.com>
Co-authored-by: islamd7 <vusal04999@gmail.com>
Co-authored-by: Valida Rahmanova <validerehmanova04@gmail.com>
Co-authored-by: yusif043-bit <yusif.abbaszade.043@gmail.com>
Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
2024-11-27 18:09:42 +00:00
pixeebot[bot]
d832a90de0 (CodeQL) Fixed finding: "Arbitrary file access during archive extraction ("Zip Slip")
" (#2344)

(CodeQL) Fixed finding: "Arbitrary file access during archive extraction ("Zip Slip")
"

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-11-27 07:16:03 +00:00
Anthony Stirling
212e521238 Update MetricsAggregatorService.java 2024-11-26 21:30:47 +00:00
github-actions[bot]
0915e72a3d 📝 Update README: Translation Progress Table (#2341)
📝 Sync README
> Made via sync_files.yml

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-11-26 20:52:54 +00:00
github-actions[bot]
ee5013651f Update 3rd Party Licenses (#2342)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 20:52:39 +00:00
github-actions[bot]
4aa44e6fc0 Update translation files (#2343)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2024-11-26 20:51:57 +00:00
github-actions[bot]
41c743a9f8 Update 3rd Party Licenses (#2337)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-26 20:51:02 +00:00
Anthony Stirling
833b3c45c6 Removal of Ghostscript to use qpdf and tesseract directly (#2338)
* navbar fix multi tool and compress location

* release notes and ghostscript removal

* cleanups

* formatting

* update docs

* more

* more

* docs

* release bump

* Hardening suggestions for Stirling-PDF / ghostscript (#2339)

* Protect `readLine()` against DoS

* Sanitized user-provided file names in HTTP multipart uploads

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>

---------

Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com>
2024-11-26 20:50:35 +00:00
Omar Ahmed Hassan
654bc94d44 Fix: input file overwrite in merge (#2335)
* Fix input files being overwritten by newly uploaded files

- Fix a bug that caused existing selected/uploaded files to be overwritten when a new input file is uploaded through input element.
- Add source property to change event to differentiate between uploaded files using input element and drag/drop uploads to avoid processing drag/drop files more than once, thus avoiding file duplication (file duplication resulting from copying drop/drop files to input files on each 'change' event).

* Dispatch and use file-input-change instead of change event for merging

- Dispatch "file-input-change" event after each "change" event in file upload, to notify other functions/components relying on the files provided by the \<input\> element.
- Use "file-input-change" instead of "change" event to display the latest version of uploaded files.

# FAQ:
- Why use "file-input-change" instead of "change" in merge.js?
= "change" event is automatically triggered when a file is uploaded through \<input\> element which would replace all the existing selected/uploaded files including the drag/drop files.

## Example:
Let's say that the user wants to upload/select the x.pdf, y.pdf and z.pdf all together:

- user selects "x.pdf" -> file selected successfully.
= selected files: x.pdf

- user drags and drops "y.pdf" -> file dropped successfully
= selected files: x.pdf, y.pdf

- user selects again using \<input\> "z.pdf" -> file selected succesfully overwriting selected files.
= selected files: z.pdf
2024-11-26 20:41:08 +00:00
dependabot[bot]
86fa404c90 Bump commons-io:commons-io from 2.17.0 to 2.18.0 (#2333)
Bumps commons-io:commons-io from 2.17.0 to 2.18.0.

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-26 20:38:51 +00:00
87 changed files with 2534 additions and 702 deletions

View File

@@ -191,43 +191,43 @@ Stirling-PDF currently supports 37 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![99%](https://geps.dev/progress/99) | | Arabic (العربية) (ar_AR) | ![98%](https://geps.dev/progress/98) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![76%](https://geps.dev/progress/76) | | Azerbaijani (Azərbaycan Dili) (az_AZ) | ![97%](https://geps.dev/progress/97) |
| Basque (Euskara) (eu_ES) | ![54%](https://geps.dev/progress/54) | | Basque (Euskara) (eu_ES) | ![54%](https://geps.dev/progress/54) |
| Bulgarian (Български) (bg_BG) | ![95%](https://geps.dev/progress/95) | | Bulgarian (Български) (bg_BG) | ![94%](https://geps.dev/progress/94) |
| Catalan (Català) (ca_CA) | ![89%](https://geps.dev/progress/89) | | Catalan (Català) (ca_CA) | ![88%](https://geps.dev/progress/88) |
| Croatian (Hrvatski) (hr_HR) | ![96%](https://geps.dev/progress/96) | | Croatian (Hrvatski) (hr_HR) | ![95%](https://geps.dev/progress/95) |
| Czech (Česky) (cs_CZ) | ![96%](https://geps.dev/progress/96) | | Czech (Česky) (cs_CZ) | ![95%](https://geps.dev/progress/95) |
| Danish (Dansk) (da_DK) | ![95%](https://geps.dev/progress/95) | | Danish (Dansk) (da_DK) | ![94%](https://geps.dev/progress/94) |
| Dutch (Nederlands) (nl_NL) | ![94%](https://geps.dev/progress/94) | | Dutch (Nederlands) (nl_NL) | ![93%](https://geps.dev/progress/93) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![98%](https://geps.dev/progress/98) | | French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) | | German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![96%](https://geps.dev/progress/96) | | Greek (Ελληνικά) (el_GR) | ![95%](https://geps.dev/progress/95) |
| Hindi (हिंदी) (hi_IN) | ![93%](https://geps.dev/progress/93) | | Hindi (हिंदी) (hi_IN) | ![92%](https://geps.dev/progress/92) |
| Hungarian (Magyar) (hu_HU) | ![96%](https://geps.dev/progress/96) | | Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![96%](https://geps.dev/progress/96) | | Indonesian (Bahasa Indonesia) (id_ID) | ![95%](https://geps.dev/progress/95) |
| Irish (Gaeilge) (ga_IE) | ![86%](https://geps.dev/progress/86) | | Irish (Gaeilge) (ga_IE) | ![85%](https://geps.dev/progress/85) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![84%](https://geps.dev/progress/84) | | Japanese (日本語) (ja_JP) | ![83%](https://geps.dev/progress/83) |
| Korean (한국어) (ko_KR) | ![94%](https://geps.dev/progress/94) | | Korean (한국어) (ko_KR) | ![93%](https://geps.dev/progress/93) |
| Norwegian (Norsk) (no_NB) | ![86%](https://geps.dev/progress/86) | | Norwegian (Norsk) (no_NB) | ![85%](https://geps.dev/progress/85) |
| Polish (Polski) (pl_PL) | ![95%](https://geps.dev/progress/95) | | Polish (Polski) (pl_PL) | ![94%](https://geps.dev/progress/94) |
| Portuguese (Português) (pt_PT) | ![96%](https://geps.dev/progress/96) | | Portuguese (Português) (pt_PT) | ![95%](https://geps.dev/progress/95) |
| Portuguese Brazilian (Português) (pt_BR) | ![96%](https://geps.dev/progress/96) | | Portuguese Brazilian (Português) (pt_BR) | ![95%](https://geps.dev/progress/95) |
| Romanian (Română) (ro_RO) | ![89%](https://geps.dev/progress/89) | | Romanian (Română) (ro_RO) | ![87%](https://geps.dev/progress/87) |
| Russian (Русский) (ru_RU) | ![95%](https://geps.dev/progress/95) | | Russian (Русский) (ru_RU) | ![94%](https://geps.dev/progress/94) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![69%](https://geps.dev/progress/69) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![68%](https://geps.dev/progress/68) |
| Simplified Chinese (简体中文) (zh_CN) | ![90%](https://geps.dev/progress/90) | | Simplified Chinese (简体中文) (zh_CN) | ![89%](https://geps.dev/progress/89) |
| Slovakian (Slovensky) (sk_SK) | ![81%](https://geps.dev/progress/81) | | Slovakian (Slovensky) (sk_SK) | ![80%](https://geps.dev/progress/80) |
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) | | Spanish (Español) (es_ES) | ![95%](https://geps.dev/progress/95) |
| Swedish (Svenska) (sv_SE) | ![95%](https://geps.dev/progress/95) | | Swedish (Svenska) (sv_SE) | ![94%](https://geps.dev/progress/94) |
| Thai (ไทย) (th_TH) | ![94%](https://geps.dev/progress/94) | | Thai (ไทย) (th_TH) | ![93%](https://geps.dev/progress/93) |
| Traditional Chinese (繁體中文) (zh_TW) | ![97%](https://geps.dev/progress/97) | | Traditional Chinese (繁體中文) (zh_TW) | ![95%](https://geps.dev/progress/95) |
| Turkish (Türkçe) (tr_TR) | ![90%](https://geps.dev/progress/90) | | Turkish (Türkçe) (tr_TR) | ![89%](https://geps.dev/progress/89) |
| Ukrainian (Українська) (uk_UA) | ![79%](https://geps.dev/progress/79) | | Ukrainian (Українська) (uk_UA) | ![78%](https://geps.dev/progress/78) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![87%](https://geps.dev/progress/87) | | Vietnamese (Tiếng Việt) (vi_VN) | ![86%](https://geps.dev/progress/86) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.) ## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)

View File

@@ -21,10 +21,12 @@ ext {
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.36"
bouncycastleVersion = "1.79" bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
} }
group = "stirling.software" group = "stirling.software"
version = "0.35.0" version = "0.35.1"
java { java {
@@ -144,17 +146,18 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation 'org.springframework.security:spring-security-saml2-service-provider:6.4.1' implementation "org.springframework.session:spring-session-core:$springBootVersion"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232" runtimeOnly "com.h2database:h2:2.3.232"
constraints { constraints {
implementation "org.opensaml:opensaml-core" implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api" implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-impl" implementation "org.opensaml:opensaml-saml-impl:$openSamlVersion"
} }
implementation "org.springframework.security:spring-security-saml2-service-provider" implementation "org.springframework.security:spring-security-saml2-service-provider:$springSecuritySamlVersion"
// implementation 'org.springframework.security:spring-security-core:$springSecuritySamlVersion'
implementation 'com.coveo:saml-client:5.0.0' implementation 'com.coveo:saml-client:5.0.0'
@@ -186,7 +189,7 @@ dependencies {
// Image metadata extractor // Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0" implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.17.0" implementation "commons-io:commons-io:2.18.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF //general PDF

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,6 @@ public class ExternalAppDepConfig {
put("unoconv", List.of("Unoconv")); put("unoconv", List.of("Unoconv"));
put("qpdf", List.of("qpdf")); put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract")); put("tesseract", List.of("tesseract"));
} }
}; };
@@ -98,7 +97,7 @@ public class ExternalAppDepConfig {
public void checkDependencies() { public void checkDependencies() {
// Check core dependencies // Check core dependencies
checkDependencyAndDisableGroup("tesseract"); checkDependencyAndDisableGroup("tesseract");
checkDependencyAndDisableGroup("soffice"); checkDependencyAndDisableGroup("soffice");
checkDependencyAndDisableGroup("qpdf"); checkDependencyAndDisableGroup("qpdf");
checkDependencyAndDisableGroup("weasyprint"); checkDependencyAndDisableGroup("weasyprint");

View File

@@ -30,6 +30,7 @@ public class InitialSecuritySetup {
initializeAdminUser(); initializeAdminUser();
} else { } else {
databaseBackupHelper.exportDatabase(); databaseBackupHelper.exportDatabase();
userService.migrateOauth2ToSSO();
} }
initializeInternalApiUser(); initializeInternalApiUser();
} }

View File

@@ -1,16 +1,20 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.*; import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
@@ -28,19 +32,35 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.saml2.core.Saml2X509Credential; import org.springframework.security.saml2.core.Saml2X509Credential;
import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType; import org.springframework.security.saml2.core.Saml2X509Credential.Saml2X509CredentialType;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.InMemoryRelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.security.saml2.provider.service.web.authentication.Saml2WebSsoAuthenticationFilter; import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding;
import org.springframework.security.saml2.provider.service.web.HttpSessionSaml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.context.SecurityContextHolderFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.session.ForceEagerSessionCreationFilter;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@@ -64,6 +84,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity @EnableMethodSecurity
@Slf4j @Slf4j
@DependsOn("runningEE")
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService; @Autowired private CustomUserDetailsService userDetailsService;
@@ -79,6 +100,10 @@ public class SecurityConfiguration {
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter; @Autowired private UserAuthenticationFilter userAuthenticationFilter;
@@ -90,13 +115,14 @@ public class SecurityConfiguration {
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) { if (loginEnabledValue) {
http.addFilterBefore( http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (!applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@@ -137,7 +163,7 @@ public class SecurityConfiguration {
http.sessionManagement( http.sessionManagement(
sessionManagement -> sessionManagement ->
sessionManagement sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10) .maximumSessions(10)
.maxSessionsPreventsLogin(false) .maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry) .sessionRegistry(sessionRegistry)
@@ -245,12 +271,23 @@ public class SecurityConfiguration {
} }
// Handle SAML // Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ() if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) { // Configure the authentication provider
http.authenticationProvider(samlAuthenticationProvider()); OpenSaml4AuthenticationProvider authenticationProvider =
http.saml2Login( new OpenSaml4AuthenticationProvider();
saml2 -> authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
.saml2Login(
saml2 -> {
try {
saml2.loginPage("/saml2") saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrations())
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler( .successHandler(
new CustomSaml2AuthenticationSuccessHandler( new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService, loginAttemptService,
@@ -258,14 +295,18 @@ public class SecurityConfiguration {
userService)) userService))
.failureHandler( .failureHandler(
new CustomSaml2AuthenticationFailureHandler()) new CustomSaml2AuthenticationFailureHandler())
.permitAll()) .authenticationRequestResolver(
.addFilterBefore( authenticationRequestResolver(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class); relyingPartyRegistrations()));
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e);
}
});
} }
} else { } else {
if (applicationProperties.getSecurity().getCsrfDisabled()) { if (!applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
CookieCsrfTokenRepository cookieRepo = CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse(); CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler = CsrfTokenRequestAttributeHandler requestHandler =
@@ -282,20 +323,6 @@ public class SecurityConfiguration {
return http.build(); return http.build();
} }
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
return authenticationProvider;
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean @Bean
@ConditionalOnProperty( @ConditionalOnProperty(
value = "security.oauth2.enabled", value = "security.oauth2.enabled",
@@ -432,11 +459,12 @@ public class SecurityConfiguration {
havingValue = "true", havingValue = "true",
matchIfMissing = false) matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception { public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2(); SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
Resource privateKeyResource = samlConf.getPrivateKey(); X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert(); Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential = Saml2X509Credential signingCredential =
@@ -445,26 +473,97 @@ public class SecurityConfiguration {
CertificateUtils.readCertificate(certificateResource), CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING); Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp = RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId()) RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials((c) -> c.add(signingCredential)) .signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata( .assertingPartyMetadata(
(details) -> metadata ->
details.entityId(samlConf.getIdpIssuer()) metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation( .singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl()) samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials( .verificationX509Credentials(
(c) -> c.add(verificationCredential)) c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true)) .wantAuthnRequestsSigned(true))
.build(); .build();
return new InMemoryRelyingPartyRegistrationRepository(rp); return new InMemoryRelyingPartyRegistrationRepository(rp);
} }
@Bean @Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public OpenSaml4AuthenticationRequestResolver authenticationRequestResolver(
RelyingPartyRegistrationRepository relyingPartyRegistrationRepository) {
OpenSaml4AuthenticationRequestResolver resolver =
new OpenSaml4AuthenticationRequestResolver(relyingPartyRegistrationRepository);
resolver.setAuthnRequestCustomizer(
customizer -> {
log.debug("Customizing SAML Authentication request");
AuthnRequest authnRequest = customizer.getAuthnRequest();
log.debug("AuthnRequest ID: {}", authnRequest.getID());
if (authnRequest.getID() == null) {
authnRequest.setID("ARQ" + UUID.randomUUID().toString());
}
log.debug("AuthnRequest new ID after set: {}", authnRequest.getID());
log.debug("AuthnRequest IssueInstant: {}", authnRequest.getIssueInstant());
log.debug(
"AuthnRequest Issuer: {}",
authnRequest.getIssuer() != null
? authnRequest.getIssuer().getValue()
: "null");
HttpServletRequest request = customizer.getRequest();
// Log HTTP request details
log.debug("HTTP Request Method: {}", request.getMethod());
log.debug("Request URI: {}", request.getRequestURI());
log.debug("Request URL: {}", request.getRequestURL().toString());
log.debug("Query String: {}", request.getQueryString());
log.debug("Remote Address: {}", request.getRemoteAddr());
// Log headers
Collections.list(request.getHeaderNames())
.forEach(
headerName -> {
log.debug(
"Header - {}: {}",
headerName,
request.getHeader(headerName));
});
// Log SAML specific parameters
log.debug("SAML Request Parameters:");
log.debug("SAMLRequest: {}", request.getParameter("SAMLRequest"));
log.debug("RelayState: {}", request.getParameter("RelayState"));
// Log session debugrmation if exists
if (request.getSession(false) != null) {
log.debug("Session ID: {}", request.getSession().getId());
}
// Log any assertions consumer service details if present
if (authnRequest.getAssertionConsumerServiceURL() != null) {
log.debug(
"AssertionConsumerServiceURL: {}",
authnRequest.getAssertionConsumerServiceURL());
}
// Log NameID policy if present
if (authnRequest.getNameIDPolicy() != null) {
log.debug(
"NameIDPolicy Format: {}",
authnRequest.getNameIDPolicy().getFormat());
}
});
return resolver;
}
public DaoAuthenticationProvider daoAuthenticationProvider() { public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService); provider.setUserDetailsService(userDetailsService);

View File

@@ -18,6 +18,7 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface; import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
@@ -50,8 +51,19 @@ public class UserService implements UserServiceInterface {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Transactional
public void migrateOauth2ToSSO() {
userRepository
.findByAuthenticationTypeIgnoreCase("OAUTH2")
.forEach(
user -> {
user.setAuthenticationType(AuthenticationType.SSO);
userRepository.save(user);
});
}
// Handle OAUTH2 login and user auto creation. // Handle OAUTH2 login and user auto creation.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException { throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) { if (!isUsernameValid(username)) {
return false; return false;
@@ -61,7 +73,7 @@ public class UserService implements UserServiceInterface {
return true; return true;
} }
if (autoCreateUser) { if (autoCreateUser) {
saveUser(username, AuthenticationType.OAUTH2); saveUser(username, AuthenticationType.SSO);
return true; return true;
} }
return false; return false;

View File

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

View File

@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -28,15 +30,26 @@ public class CertificateUtils {
} }
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception { public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PemReader pemReader = try (PEMParser pemParser =
new PemReader( new PEMParser(
new InputStreamReader( new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) { privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedKey = pemObject.getContent(); Object object = pemParser.readObject();
return (RSAPrivateKey) JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey)); if (object instanceof PEMKeyPair) {
// Handle traditional RSA private key format
PEMKeyPair keypair = (PEMKeyPair) object;
return (RSAPrivateKey) converter.getPrivateKey(keypair.getPrivateKeyInfo());
} else if (object instanceof PrivateKeyInfo) {
// Handle PKCS#8 format
return (RSAPrivateKey) converter.getPrivateKey((PrivateKeyInfo) object);
} else {
throw new IllegalArgumentException(
"Unsupported key format: "
+ (object != null ? object.getClass().getName() : "null"));
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import io.github.pixee.security.BoundedLineReader;
import io.github.pixee.security.Filenames;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
@@ -35,6 +33,8 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.BoundedLineReader;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -176,7 +176,9 @@ public class OCRController {
// Read the final PDF file // Read the final PDF file
byte[] pdfContent = Files.readAllBytes(finalOutputFile); byte[] pdfContent = Files.readAllBytes(finalOutputFile);
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()).replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_OCR.pdf";
return ResponseEntity.ok() return ResponseEntity.ok()
.header( .header(

View File

@@ -50,7 +50,6 @@ public class RepairController {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
// Save the uploaded file to a temporary location // Save the uploaded file to a temporary location
Path tempInputFile = Files.createTempFile("input_", ".pdf"); Path tempInputFile = Files.createTempFile("input_", ".pdf");
Path tempOutputFile = Files.createTempFile("output_", ".pdf");
byte[] pdfBytes = null; byte[] pdfBytes = null;
inputFile.transferTo(tempInputFile.toFile()); inputFile.transferTo(tempInputFile.toFile());
try { try {
@@ -61,14 +60,13 @@ public class RepairController {
command.add("--qdf"); // Linearizes and normalizes PDF structure command.add("--qdf"); // Linearizes and normalizes PDF structure
command.add("--object-streams=disable"); // Can help with some corruptions command.add("--object-streams=disable"); // Can help with some corruptions
command.add(tempInputFile.toString()); command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = ProcessExecutorResult returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command); .runCommandWithOutputHandling(command);
// Read the optimized PDF file // Read the optimized PDF file
pdfBytes = pdfDocumentFactory.loadToBytes(tempOutputFile.toFile()); pdfBytes = pdfDocumentFactory.loadToBytes(tempInputFile.toFile());
// Return the optimized PDF as a response // Return the optimized PDF as a response
String outputFilename = String outputFilename =
@@ -79,7 +77,6 @@ public class RepairController {
} finally { } finally {
// Clean up the temporary files // Clean up the temporary files
Files.deleteIfExists(tempInputFile); Files.deleteIfExists(tempInputFile);
Files.deleteIfExists(tempOutputFile);
} }
} }
} }

View File

@@ -147,7 +147,7 @@ public class StampController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) Filenames.toSimpleFileName(pdfFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_stamped.pdf"); + "_stamped.pdf");
} }
@@ -191,7 +191,7 @@ public class StampController {
String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf("."));
File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile(); File tempFile = Files.createTempFile("NotoSansFont", fileExtension).toFile();
try (InputStream is = classPathResource.getInputStream(); try (InputStream is = classPathResource.getInputStream();
FileOutputStream os = new FileOutputStream(tempFile)) { FileOutputStream os = new FileOutputStream(tempFile)) {
IOUtils.copy(is, os); IOUtils.copy(is, os);
font = PDType0Font.load(document, tempFile); font = PDType0Font.load(document, tempFile);
} finally { } finally {
@@ -229,10 +229,22 @@ public class StampController {
calculatePositionY( calculatePositionY(
pageSize, position, calculateTextCapHeight(font, fontSize), margin); pageSize, position, calculateTextCapHeight(font, fontSize), margin);
} }
// Split the stampText into multiple lines
String[] lines = stampText.split("\\\\n");
// Calculate dynamic line height based on font ascent and descent
float ascent = font.getFontDescriptor().getAscent();
float descent = font.getFontDescriptor().getDescent();
float lineHeight = ((ascent - descent) / 1000) * fontSize;
contentStream.beginText(); contentStream.beginText();
contentStream.setTextMatrix(Matrix.getRotateInstance(Math.toRadians(rotation), x, y)); for (int i = 0; i < lines.length; i++) {
contentStream.showText(stampText); String line = lines[i];
// Set the text matrix for each line with rotation
contentStream.setTextMatrix(
Matrix.getRotateInstance(Math.toRadians(rotation), x, y - (i * lineHeight)));
contentStream.showText(line);
}
contentStream.endText(); contentStream.endText();
} }

View File

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

View File

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

View File

@@ -39,6 +39,13 @@ public class MetricsAggregatorService {
if (method == null || uri == null) { if (method == null || uri == null) {
return; return;
} }
if (!method.equals("GET") && !method.equals("POST")) {
return;
}
// Skip URIs that are 2 characters or shorter
if (uri.length() <= 2) {
return;
}
String key = String key =
String.format( String.format(

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
package stirling.software.SPDF.utils.propertyeditor;
import java.beans.PropertyEditorSupport;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class StringToMapPropertyEditor extends PropertyEditorSupport {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void setAsText(String text) throws IllegalArgumentException {
try {
TypeReference<HashMap<String, String>> typeRef =
new TypeReference<HashMap<String, String>>() {};
Map<String, String> map = objectMapper.readValue(text, typeRef);
setValue(map);
} catch (Exception e) {
throw new IllegalArgumentException(
"Failed to convert java.lang.String to java.util.Map");
}
}
}

View File

@@ -3,6 +3,10 @@ multipart.enabled=true
logging.level.org.springframework=WARN logging.level.org.springframework=WARN
logging.level.org.hibernate=WARN logging.level.org.hibernate=WARN
logging.level.org.eclipse.jetty=WARN logging.level.org.eclipse.jetty=WARN
#logging.level.org.springframework.security.saml2=TRACE
#logging.level.org.springframework.security=DEBUG
#logging.level.org.opensaml=DEBUG
#logging.level.stirling.software.SPDF.config.security: DEBUG
logging.level.com.zaxxer.hikari=WARN logging.level.com.zaxxer.hikari=WARN
spring.jpa.open-in-view=false spring.jpa.open-in-view=false
@@ -27,6 +31,8 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
spring.devtools.restart.enabled=true spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=تحريك إلى اليسار
multiTool.moveRight=تحريك إلى اليمين multiTool.moveRight=تحريك إلى اليمين
multiTool.delete=حذف multiTool.delete=حذف
multiTool.dragDropMessage=الصفحات المحددة multiTool.dragDropMessage=الصفحات المحددة
multiTool.undo=تراجع
multiTool.redo=إعادة إجراء
#multiTool-advert #multiTool-advert
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية! multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=مستوى الإشارة المرجعية: اختر مس
splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ. splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة. splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
splitByChapters.submit=تقطيع ملف PDF splitByChapters.submit=تقطيع ملف PDF
#File Chooser
fileChooser.click=انقر هنا
fileChooser.or=أو
fileChooser.dragAndDrop=قم بسحب الملفات وإفلاتها
fileChooser.hoveredDragAndDrop=قم بسحب المفات وإفلاتها هنا
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -34,10 +34,10 @@ sizes.small=Kiçik
sizes.medium=Orta sizes.medium=Orta
sizes.large=Böyük sizes.large=Böyük
sizes.x-large=Ekstra Böyük sizes.x-large=Ekstra Böyük
error.pdfPassword=PDF sənədi şifrələnmişdir və şifrə təmin edilməmişdir və ya yanlışdır. error.pdfPassword=PDF sənədi şifrlənmişdir və şifr təmin edilməmişdir və ya yanlışdır.
delete=Sil delete=Sil
username=İstifadəçi Adı username=İstifadəçi Adı
password=Şifrə password=Şifr
welcome=Xoş gəldiniz welcome=Xoş gəldiniz
property=Xüsusiyyət property=Xüsusiyyət
black=Qara black=Qara
@@ -53,11 +53,11 @@ no=Xeyr
changedCredsMessage=Etibarnamələr dəyişdirildi! changedCredsMessage=Etibarnamələr dəyişdirildi!
notAuthenticatedMessage=İstifadəçinin kimliyi təsdiqlənməyib. notAuthenticatedMessage=İstifadəçinin kimliyi təsdiqlənməyib.
userNotFoundMessage=İstifadəçi tapılmadı. userNotFoundMessage=İstifadəçi tapılmadı.
incorrectPasswordMessage=Cari şifrə yanlışdır. incorrectPasswordMessage=Cari şifr yanlışdır.
usernameExistsMessage=İstifadəçi adı mövcuddur. usernameExistsMessage=İstifadəçi adı mövcuddur.
invalidUsernameMessage=Yanlış istifadəçi adı, istifadəçi adı sadəcə hərflərdən, rəqəmlərdən və @._+- xüsusi simvollarından ibarət ola bilər və ya düzgün email ünvanı olmalıdır. invalidUsernameMessage=Yanlış istifadəçi adı, istifadəçi adı sadəcə hərflərdən, rəqəmlərdən və @._+- xüsusi simvollarından ibarət ola bilər və ya düzgün email ünvanı olmalıdır.
invalidPasswordMessage=Şifrə boş olmamalıdır, başlanğıc və sonunda boşluqdan istifadə edilməməlidir. invalidPasswordMessage=Şifr boş olmamalıdır, başlanğıc və sonunda boşluqdan istifadə edilməməlidir.
confirmPasswordErrorMessage=Yeni Şifrə və Yeni Şifrəni Doğrula uyğun olmalıdır. confirmPasswordErrorMessage=Yeni Şifr və Yeni Şifri Doğrula uyğun olmalıdır.
deleteCurrentUserMessage=Hazırda daxil olmuş istifadəçini silmək mümkün deyil. deleteCurrentUserMessage=Hazırda daxil olmuş istifadəçini silmək mümkün deyil.
deleteUsernameExistsMessage=İstifadəçi adı mövcud deyildir və silinə bilməz. deleteUsernameExistsMessage=İstifadəçi adı mövcud deyildir və silinə bilməz.
downgradeCurrentUserMessage=Cari istifadəçinin rolunu aşağı salmaq mümkün deyil downgradeCurrentUserMessage=Cari istifadəçinin rolunu aşağı salmaq mümkün deyil
@@ -70,8 +70,8 @@ oops=Oops!
help=Yardım help=Yardım
goHomepage=Ana səhifəyə get goHomepage=Ana səhifəyə get
joinDiscord=Discord serverimizə qatıl joinDiscord=Discord serverimizə qatıl
seeDockerHub=Docker Hub'a bax seeDockerHub=Docker Hub-a bax
visitGithub=Github Repository'ə Baş Çək visitGithub=Github Repository-ə Baş Çək
donate=İanə Ver donate=İanə Ver
color=Rəng color=Rəng
sponsor=Sponsor sponsor=Sponsor
@@ -126,9 +126,9 @@ enterpriseEdition.ssoAdvert=Daha çox istifadəçi-idarəetmə xüsusiyyətləri
################# #################
# Analytics # # Analytics #
################# #################
analytics.title=Stirling PDF'i daha yaxşı etmək istəyirsinizmi? analytics.title=Stirling PDF-i daha yaxşı etmək istəyirsinizmi?
analytics.paragraph1=Stirling PDF bizə məhsulu inkişaf etdirməyə kömək etmək üçün analitikaya üstünlük verib. Biz heç bir şəxsi məlumatı və ya fayl məzmununu izləmirik. analytics.paragraph1=Stirling PDF bizə məhsulu inkişaf etdirməyə kömək etmək üçün analitikaya üstünlük verib. Biz heç bir şəxsi məlumatı və ya fayl məzmununu izləmirik.
analytics.paragraph2=Zəhmət olmasa, Stringling-PDF'ə inkişaf etməkdə və istifadəçilərimizi daha yaxşı anlamaqda yardım etmək üçün analitikanı aktivləşdirməyi nəzərə alın. analytics.paragraph2=Zəhmət olmasa, Stringling-PDF-ə inkişaf etməkdə və istifadəçilərimizi daha yaxşı anlamaqda yardım etmək üçün analitikanı aktivləşdirməyi nəzərə alın.
analytics.enable=Analitikanı aktivləşdir analytics.enable=Analitikanı aktivləşdir
analytics.disable=Analitikanı deaktivləşdir analytics.disable=Analitikanı deaktivləşdir
analytics.settings=Analitikanın parametrlərini config/settings.yml faylından dəyişə bilərsiniz. analytics.settings=Analitikanın parametrlərini config/settings.yml faylından dəyişə bilərsiniz.
@@ -141,11 +141,11 @@ navbar.darkmode=Qaranlıq Tema
navbar.language=Dillər navbar.language=Dillər
navbar.settings=Parametrlər navbar.settings=Parametrlər
navbar.allTools=Alətlər navbar.allTools=Alətlər
navbar.multiTool=Çox Alət navbar.multiTool=Multi-Alət
navbar.search=Axtar navbar.search=Axtar
navbar.sections.organize=Təşkil et navbar.sections.organize=Təşkil et
navbar.sections.convertTo=PDF'ə Çevir navbar.sections.convertTo=PDF-ə Çevir
navbar.sections.convertFrom=PDF'dən Çevir navbar.sections.convertFrom=PDF-dən Çevir
navbar.sections.security=İmza & Təhlükəsizlik navbar.sections.security=İmza & Təhlükəsizlik
navbar.sections.advance=Qabaqcıl navbar.sections.advance=Qabaqcıl
navbar.sections.edit=Bax & Redaktə et navbar.sections.edit=Bax & Redaktə et
@@ -171,11 +171,11 @@ settings.cacheInputs.help=Gələcək əməliyyatlar üçün əvvəllər istifad
changeCreds.title=Məlumatları dəyişdirin changeCreds.title=Məlumatları dəyişdirin
changeCreds.header=Hesab Məlumatlarınızı Yeniləyin changeCreds.header=Hesab Məlumatlarınızı Yeniləyin
changeCreds.changePassword=Siz standart giriş məlumatlarından istifadə edirsiniz. Zəhmət olmasa, yeni şifrə daxil edin changeCreds.changePassword=Siz standart giriş məlumatlarından istifadə edirsiniz. Zəhmət olmasa, yeni şifr daxil edin
changeCreds.newUsername=Yeni İstifadəçi Adı changeCreds.newUsername=Yeni İstifadəçi Adı
changeCreds.oldPassword=Cari Şifrə changeCreds.oldPassword=Cari Şifr
changeCreds.newPassword=Yeni Şifrə changeCreds.newPassword=Yeni Şifr
changeCreds.confirmNewPassword=Yeni Şifrəni Təsdiqləyin changeCreds.confirmNewPassword=Yeni Şifri Təsdiqləyin
changeCreds.submit=Dəyişiklikləri Təsdiqlə changeCreds.submit=Dəyişiklikləri Təsdiqlə
@@ -186,11 +186,11 @@ account.adminSettings=Admin Paramterləri - İstifadəçilər Əlavə Et və Onl
account.userControlSettings=İstifadəçi İdarəetmə Parametrləri account.userControlSettings=İstifadəçi İdarəetmə Parametrləri
account.changeUsername=İstifadəçi Adını Dəyiş account.changeUsername=İstifadəçi Adını Dəyiş
account.newUsername=Yeni İstifadəçi Adı account.newUsername=Yeni İstifadəçi Adı
account.password=Təsdiqləmə Şifrəsi account.password=Təsdiqləmə Şifri
account.oldPassword=Keçmiş Şifrə account.oldPassword=Keçmiş Şifr
account.newPassword=Yeni Şifrə account.newPassword=Yeni Şifr
account.changePassword=Şifrəni Dəyiş account.changePassword=Şifri Dəyiş
account.confirmNewPassword=Yeni Şifrəni Təsdiqlə account.confirmNewPassword=Yeni Şifri Təsdiqlə
account.signOut=Çıxış account.signOut=Çıxış
account.yourApiKey=Sizin API Açarınız account.yourApiKey=Sizin API Açarınız
account.syncTitle=Brauzer parametrlərini hesabla sinxronlaşdırın account.syncTitle=Brauzer parametrlərini hesabla sinxronlaşdırın
@@ -543,7 +543,7 @@ login.title=Daxil olun
login.header=Daxil olun login.header=Daxil olun
login.signin=Daxil olun login.signin=Daxil olun
login.rememberme=Məni xatırla login.rememberme=Məni xatırla
login.invalid=Etibarsız istifadəçi adı və ya şifrə. login.invalid=Etibarsız istifadəçi adı və ya şifr.
login.locked=Sizin hesabınız kilidlənmişdir. login.locked=Sizin hesabınız kilidlənmişdir.
login.signinTitle=Zəhmət olmasa, daxil olun login.signinTitle=Zəhmət olmasa, daxil olun
login.ssoSignIn=Single Sign-on vasitəsilə daxil olun login.ssoSignIn=Single Sign-on vasitəsilə daxil olun
@@ -581,8 +581,8 @@ showJS.submit=Göstər
#pdfToSinglePage #pdfToSinglePage
pdfToSinglePage.title=PDF'dən Tək Səhifəyə pdfToSinglePage.title=PDF-dən Tək Səhifəyə
pdfToSinglePage.header=PDF'dən Tək Səhifəyə pdfToSinglePage.header=PDF-dən Tək Səhifəyə
pdfToSinglePage.submit=Tək Səhifəyə Çevir pdfToSinglePage.submit=Tək Səhifəyə Çevir
@@ -601,8 +601,8 @@ getPdfInfo.downloadJson=JSON yüklə
#markdown-to-pdf #markdown-to-pdf
MarkdownToPDF.title=Markdown'dan PDF'ə MarkdownToPDF.title=Markdown-dan PDF-ə
MarkdownToPDF.header=Markdown'dan PDF'ə MarkdownToPDF.header=Markdown-dan PDF-ə
MarkdownToPDF.submit=Çevir MarkdownToPDF.submit=Çevir
MarkdownToPDF.help=İş davam edir MarkdownToPDF.help=İş davam edir
MarkdownToPDF.credit=WeasyPrint İstifadə Edir MarkdownToPDF.credit=WeasyPrint İstifadə Edir
@@ -617,8 +617,8 @@ URLToPDF.credit=WeasyPrint İstifadə Edir
#html-to-pdf #html-to-pdf
HTMLToPDF.title=HTML'dən PDF'ə HTMLToPDF.title=HTML-dən PDF-ə
HTMLToPDF.header=HTML'dən PDF'ə HTMLToPDF.header=HTML-dən PDF-ə
HTMLToPDF.help=HTML fayllarını və tərkibində mütləq html/css/images və s. olan ZIP fayllarını qəbul edir HTMLToPDF.help=HTML fayllarını və tərkibində mütləq html/css/images və s. olan ZIP fayllarını qəbul edir
HTMLToPDF.submit=Çevir HTMLToPDF.submit=Çevir
HTMLToPDF.credit=WeasyPrint İstifadə Edir HTMLToPDF.credit=WeasyPrint İstifadə Edir
@@ -656,14 +656,14 @@ AddStampRequest.submit=Təsdiqlə
#sanitizePDF #sanitizePDF
sanitizePDF.title=PDF'i Təmizlə sanitizePDF.title=PDF-i Təmizlə
sanitizePDF.header=PDF Faylını Təmizlə sanitizePDF.header=PDF Faylını Təmizlə
sanitizePDF.selectText.1=JavaScript Fəaliyyətlərini Sil sanitizePDF.selectText.1=JavaScript Fəaliyyətlərini Sil
sanitizePDF.selectText.2=Daxil Edilmiş Faylları Sil sanitizePDF.selectText.2=Daxil Edilmiş Faylları Sil
sanitizePDF.selectText.3=Metadatanı Sil sanitizePDF.selectText.3=Metadatanı Sil
sanitizePDF.selectText.4=Linkləri Sil sanitizePDF.selectText.4=Linkləri Sil
sanitizePDF.selectText.5=Şriftləri Sil sanitizePDF.selectText.5=Şriftləri Sil
sanitizePDF.submit=PDF'i Təmizlə sanitizePDF.submit=PDF-i Təmizlə
#addPageNumbers #addPageNumbers
@@ -683,7 +683,7 @@ addPageNumbers.submit=Səhifə Nömrələri əlavə edin
#auto-rename #auto-rename
auto-rename.title=Avtomatik Yenidən Adlandır auto-rename.title=Avtomatik Yenidən Adlandır
auto-rename.header=Pdf'in Adını Avtomatik Yenidən Adlandır auto-rename.header=Pdf-in Adını Avtomatik Yenidən Adlandır
auto-rename.submit=Avtomatik Yenidən Adlandır auto-rename.submit=Avtomatik Yenidən Adlandır
@@ -698,7 +698,7 @@ adjustContrast.download=Yüklə
#crop #crop
crop.title=Kəs crop.title=Kəs
crop.header=Pdf'ləri Kəs crop.header=Pdf-ləri Kəs
crop.submit=Təsdiq Et crop.submit=Təsdiq Et
@@ -722,8 +722,8 @@ pipeline.title=Pipeline
#pageLayout #pageLayout
pageLayout.title=Çoxsəhifəli Sxem pageLayout.title=Çoxsəhifəli Tərtibat
pageLayout.header=Çoxsəhifəli Sxem pageLayout.header=Çoxsəhifəli Tərtibat
pageLayout.pagesPerSheet=Vərəqdəki Səhifə Sayı: pageLayout.pagesPerSheet=Vərəqdəki Səhifə Sayı:
pageLayout.addBorder=Çərçivə Əlavə Et pageLayout.addBorder=Çərçivə Əlavə Et
pageLayout.submit=Təsdiq et pageLayout.submit=Təsdiq et
@@ -739,22 +739,22 @@ scalePages.submit=Təsdiq edin
#certSign #certSign
certSign.title=Certificate Signing certSign.title=Sertifikatla İmzala
certSign.header=Sign a PDF with your certificate (Work in progress) certSign.header=PDF-i Sertifikatınızla İmzalayın (İşlənilir)
certSign.selectPDF=Select a PDF File for Signing: certSign.selectPDF=İmzalamaq üçün PDF Faylı seçin:
certSign.jksNote=Note: If your certificate type is not listed below, please convert it to a Java Keystore (.jks) file using the keytool command line tool. Then, choose the .jks file option below. certSign.jksNote=Note: Əgər sertifikatınızın tipi aşağıda göstərilməyibsə, zəhmət olmasa "Keytool command line tool" istifadə edərək onu "Java Keystroke" (.jks) faylına çevirin. Sonra, aşağıdan .jks faylını seçin.
certSign.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der): certSign.selectKey=Şəxsi Açar faylınızı seçin (PKCS#8 format, .pem və ya .der ola bilər):
certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der): certSign.selectCert=Sertifikat faylınızı seçin (X.509 format, .pem və ya .der ola bilər):
certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate): certSign.selectP12=PKCS#12 Keystore Faylınızı seçin (.p12 və ya .pfx) (İstəyə bağlı, əgər təmin olunarsa, şəxsi açar və sertifikatınızı ehtiva etməlidir):
certSign.selectJKS=Select Your Java Keystore File (.jks or .keystore): certSign.selectJKS=Java Keystore Faylınızı seçin (.jks və ya .keystore):
certSign.certType=Certificate Type certSign.certType=Sertifikat Tipi
certSign.password=Enter Your Keystore or Private Key Password (If Any): certSign.password=Keystore və ya Şəxsi Açar daxil edin (Əgər varsa):
certSign.showSig=Show Signature certSign.showSig=İmzanı Göstər
certSign.reason=Reason certSign.reason=Səbəb
certSign.location=Location certSign.location=Məkan
certSign.name=Name certSign.name=Ad
certSign.showLogo=Show Logo certSign.showLogo=Loqonu Göstər
certSign.submit=Sign PDF certSign.submit=PDF-i İmzala
#removeCertSign #removeCertSign
@@ -765,13 +765,13 @@ removeCertSign.submit=İmzanı silin
#removeBlanks #removeBlanks
removeBlanks.title=Remove Blanks removeBlanks.title=Boş Səhifələri Sil
removeBlanks.header=Remove Blank Pages removeBlanks.header=Boş SƏhifələri Silir
removeBlanks.threshold=Pixel Whiteness Threshold: removeBlanks.threshold=Minimal Piksel Bəyazlığı:
removeBlanks.thresholdDesc=Threshold for determining how white a white pixel must be to be classed as 'White'. 0 = Black, 255 pure white. removeBlanks.thresholdDesc=Pikselin "Ağ" hesab olunması üçün minimal nə qədər bəyaz olmalı olduğunu təyin edin. 0 = Qara, 255 Ağappaq.
removeBlanks.whitePercent=White Percent (%): removeBlanks.whitePercent=Bəyaz Faizi (%):
removeBlanks.whitePercentDesc=Percent of page that must be 'white' pixels to be removed removeBlanks.whitePercentDesc=Silinmək üçün səhifənin neçə faizi "ağ" piksellərdən təşkil olunmalıdır
removeBlanks.submit=Remove Blanks removeBlanks.submit=Boş Səhifələri Sil
#removeAnnotations #removeAnnotations
@@ -781,16 +781,16 @@ removeAnnotations.submit=Sil
#compare #compare
compare.title=Compare compare.title=Müqayisə Et
compare.header=Compare PDFs compare.header=PDF-ləri Müqayisə Et
compare.highlightColor.1=Highlight Color 1: compare.highlightColor.1=Önə Çıxarma Rəngi 1:
compare.highlightColor.2=Highlight Color 2: compare.highlightColor.2=Önə Çıxarma Rəngi 2:
compare.document.1=Document 1 compare.document.1=Sənəd 1
compare.document.2=Document 2 compare.document.2=Sənəd 2
compare.submit=Compare compare.submit=Müqayisə Et
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced compare.complex.message=Fayllardan biri və ya ikisi də böyük fayldır. Müqayisə effektivliyi azala bilər.
compare.large.file.message=One or Both of the provided documents are too large to process compare.large.file.message=Fayllardan biri və ya ikisi də işləmək üçün çox böyükdür.
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison. compare.no.text.message=Fayllardan birində və ya ikisində də mətn məzmunu yoxdur. Zəhmət olmasa, müqayisə üçün mətn məzmunlu PDF seçin.
#BookToPDF #BookToPDF
BookToPDF.title=Kitabları və Komiksləri PDF-ə BookToPDF.title=Kitabları və Komiksləri PDF-ə
@@ -818,12 +818,12 @@ sign.save=İmzanı yadda Saxla
sign.personalSigs=Şəxsi İmzalar sign.personalSigs=Şəxsi İmzalar
sign.sharedSigs=Paylaşılan İmzalar sign.sharedSigs=Paylaşılan İmzalar
sign.noSavedSigs=Saxlanmış imza tapılmadı sign.noSavedSigs=Saxlanmış imza tapılmadı
sign.addToAll=Add to all pages sign.addToAll=Bütün səhiflərə əlavə et
sign.delete=Delete sign.delete=Sil
sign.first=First page sign.first=İlk səhifə
sign.last=Last page sign.last=Son səhifə
sign.next=Next page sign.next=Növbəti səhifə
sign.previous=Previous page sign.previous=Əvvəlki səhifə
#repair #repair
repair.title=Bərpa Et repair.title=Bərpa Et
@@ -839,37 +839,37 @@ flatten.submit=Düzləşdirin
#ScannerImageSplit #ScannerImageSplit
ScannerImageSplit.selectText.1=Angle Threshold: ScannerImageSplit.selectText.1=Bucaq Aşağı Limiti:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10). ScannerImageSplit.selectText.2=Şəklin fırladılması üçün lazım olan minimal mütləq bucağı təyin edir (defolt: 10).
ScannerImageSplit.selectText.3=Tolerance: ScannerImageSplit.selectText.3=Rəng Toleransı:
ScannerImageSplit.selectText.4=Determines the range of color variation around the estimated background color (default: 30). ScannerImageSplit.selectText.4=Təxmin olunan arxaplan rənginin ətrafındakı rəng fərqliliyi intervalını təyin edir (defolt: 30).
ScannerImageSplit.selectText.5=Minimum Area: ScannerImageSplit.selectText.5=Minimal Sahə:
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000). ScannerImageSplit.selectText.6=Foto üçün minimal sahənin aşağı limitini təyin edir (defolt: 10000).
ScannerImageSplit.selectText.7=Minimum Contour Area: ScannerImageSplit.selectText.7=Minimal Kontur Sahəsi:
ScannerImageSplit.selectText.8=Sets the minimum contour area threshold for a photo ScannerImageSplit.selectText.8=Fotonun kontur sahəsi üçün minimal aşağı limiti təyin edir
ScannerImageSplit.selectText.9=Border Size: ScannerImageSplit.selectText.9=Sərhəd Ölçüsü:
ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1). ScannerImageSplit.selectText.10=Faylda ağ sərhədlərin olmasının qarşısını almaq üçün əlavə ediləcək sərhədin ölçüsünü təyin edir (defolt: 1).
ScannerImageSplit.info=Python is not installed. It is required to run. ScannerImageSplit.info=Python yüklənməyib. İşə salmaq üçün Python lazımdır.
#OCR #OCR
ocr.title=OCR / Scan Cleanup ocr.title=OST (OCR) / Skan Təmizləmə
ocr.header=Cleanup Scans / OCR (Optical Character Recognition) ocr.header=Skanları Təmizlə / OST (Optik Simvol Tanınması)
ocr.selectText.1=Select languages that are to be detected within the PDF (Ones listed are the ones currently detected): ocr.selectText.1=PDF-də aşkar olunacaq dilləri seçin (Göstərilmiş dillər hazırda aşkar olunmuşlardır):
ocr.selectText.2=Produce text file containing OCR text alongside the OCR'ed PDF ocr.selectText.2=OST-lənmiş PDF ilə yanaşı daxilində OST edilmiş mətn olan PDF yaradın
ocr.selectText.3=Correct pages were scanned at a skewed angle by rotating them back into place ocr.selectText.3=Əyri skan olunmuş səhifələri yerinə fırladaraq düzəldin
ocr.selectText.4=Clean page so its less likely that OCR will find text in background noise. (No output change) ocr.selectText.4=OST-in arxaplandakı artıq mətni aşkar etməsinin qarşısını almaq üçün səhifəni təmizləyin. (Çıxış dəyişmir)
ocr.selectText.5=Clean page so its less likely that OCR will find text in background noise, maintains cleanup in output. ocr.selectText.5=OST-in arxaplandakı artıq mətni aşkar etməsinin qarşısını almaq üçün səhifəni təmizləyin, təmizləməni çıxışa verilən faylda saxlayır.
ocr.selectText.6=Ignores pages that have interactive text on them, only OCRs pages that are images ocr.selectText.6=Üzərində interaktiv yazı olan səhifələri nəzərə almır, yalnız şəkil olan səhifələri OST edir.
ocr.selectText.7=Force OCR, will OCR Every page removing all original text elements ocr.selectText.7=OST-ə məcbur et, bütün orijinal mətn elementlərini silərək hər səhifəni OST edir
ocr.selectText.8=Normal (Will error if PDF contains text) ocr.selectText.8=Normal (PDF-də mətn varsa, xəta verəcək)
ocr.selectText.9=Additional Settings ocr.selectText.9=Əlavə Parametrlər
ocr.selectText.10=OCR Mode ocr.selectText.10=OST (OCR) Rejimi
ocr.selectText.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step) ocr.selectText.11=OST-dən sonra şəkilləri sil (BÜTÜN şəkilləri silir, ancaq çevirmə prosesinin bir hissəsi olduqda işə yarayır)
ocr.selectText.12=Render Type (Advanced) ocr.selectText.12=Render Tipi (Qabaqcıl)
ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker ocr.help=Bunu digər dillər üçün necə istifadə etmək və/və ya docker-də istifadə etməmək üçün bu dokumentasiyanı oxuyun
ocr.credit=This service uses qpdf and Tesseract for OCR. ocr.credit=Bu servis OST (OCR) üçün "OCRmyPDF" və "Tesseract" istifadə edir.
ocr.submit=Process PDF with OCR ocr.submit=PDF-i OST ilə işlə
#extractImages #extractImages
@@ -890,15 +890,15 @@ fileToPDF.submit=PDF-ə Çevir
#compress #compress
compress.title=Compress compress.title=Sıxışdır
compress.header=Compress PDF compress.header=PDF-i Sıxışdır
compress.credit=This service uses qpdf for PDF Compress/Optimisation. compress.credit=Bu servis PDF sıxışdırılması/Optimizasiyası üçün Ghostscript istifadə edir.
compress.selectText.1=Manual Mode - From 1 to 4 compress.selectText.1=Manual Mod - 1-dən 4-ə
compress.selectText.2=Optimization level: compress.selectText.2=Optimizasiya səviyyəsi:
compress.selectText.3=4 (Terrible for text images) compress.selectText.3=4 (Mətn şəkilləri üçün yaxşı deyil)
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size compress.selectText.4=Avto mod - PDF-in dəqiq ölçüsünü əldə etmək üçün keyfiyyəti avtomatik tənzimləyir
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB) compress.selectText.5=Gözlənilən PDF Ölçüsü (məsələn, 25MB, 10.8MB, 25KB)
compress.submit=Compress compress.submit=Sıxışdır
#Add image #Add image
@@ -919,35 +919,35 @@ merge.submit=Birləşdirin
#pdfOrganiser #pdfOrganiser
pdfOrganiser.title=Page Organiser pdfOrganiser.title=Səhifə Tənzimləyicisi
pdfOrganiser.header=PDF Page Organiser pdfOrganiser.header=PDF Səhifə Tənzimləyicisi
pdfOrganiser.submit=Rearrange Pages pdfOrganiser.submit=Səhifələri Yenidən Təşkil Edin
pdfOrganiser.mode=Mode pdfOrganiser.mode=Rejim
pdfOrganiser.mode.1=Custom Page Order pdfOrganiser.mode.1=Fərdi Səhifə Düzülüşü
pdfOrganiser.mode.2=Reverse Order pdfOrganiser.mode.2=Tərs Düzülüş
pdfOrganiser.mode.3=Duplex Sort pdfOrganiser.mode.3=İkitərəfli Çeşidləmə
pdfOrganiser.mode.4=Booklet Sort pdfOrganiser.mode.4=Kitabça Çeşidləmə
pdfOrganiser.mode.5=Side Stitch Booklet Sort pdfOrganiser.mode.5=Yan Tikiş Kitabçasının Çeşidlənməsi
pdfOrganiser.mode.6=Odd-Even Split pdfOrganiser.mode.6=Tək-Cüt Bölünmə
pdfOrganiser.mode.7=Remove First pdfOrganiser.mode.7=Birincini Sil
pdfOrganiser.mode.8=Remove Last pdfOrganiser.mode.8=Sonuncunu Sil
pdfOrganiser.mode.9=Remove First and Last pdfOrganiser.mode.9=Birinci və Sonuncunu Sil
pdfOrganiser.mode.10=Odd-Even Merge pdfOrganiser.mode.10=Tək-Cüt Birləşdirmə
pdfOrganiser.placeholder=(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1) pdfOrganiser.placeholder=(məs., 1,3,2 və ya 4-8,2,10-12 və ya 2n-1)
#multiTool #multiTool
multiTool.title=PDF Multi Tool multiTool.title=PDF Multi-Alət
multiTool.header=PDF Multi Tool multiTool.header=PDF Multi-Alət
multiTool.uploadPrompts=File Name multiTool.uploadPrompts=Fayl Adı
multiTool.selectAll=Select All multiTool.selectAll=Hamısını Seç
multiTool.deselectAll=Deselect All multiTool.deselectAll=Hamısını Seçməni Ləğv Et
multiTool.selectPages=Page Select multiTool.selectPages=Səhifə Seçimi
multiTool.selectedPages=Selected Pages multiTool.selectedPages=Seçilmiş Səhifələr
multiTool.page=Page multiTool.page=Səhifə
multiTool.deleteSelected=Delete Selected multiTool.deleteSelected=Seçilmişi Sil
multiTool.downloadAll=Export multiTool.downloadAll=İxrac Et
multiTool.downloadSelected=Export Selected multiTool.downloadSelected=Seçilmişi İxrac Et
multiTool.insertPageBreak=Insert Page Break multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File multiTool.addFile=Add File
@@ -957,10 +957,12 @@ multiTool.split=Split
multiTool.moveLeft=Move Left multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Seçilmiş Səhifə(lər)
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=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!
#view pdf #view pdf
viewPdf.title=PDF-ə baxın viewPdf.title=PDF-ə baxın
@@ -982,32 +984,32 @@ rotate.submit=Fırladın
#split-pdfs #split-pdfs
split.title=Split PDF split.title=PDF-i Bölün
split.header=Split PDF split.header=PDF-i Bölün
split.desc.1=The numbers you select are the page number you wish to do a split on split.desc.1=Seçdiyiniz Nömrələr Bölmək İstədiyiniz Səhifə Nömrəsidir
split.desc.2=As such selecting 1,3,7-9 would split a 10 page document into 6 separate PDFS with: split.desc.2=Beləliklə, 1,3,7-9 Seçimi 10 Səhifəlik Sənədi 6 Ayrı PDF-ə Böləcək:
split.desc.3=Document #1: Page 1 split.desc.3=Sənəd #1: Səhifə 1
split.desc.4=Document #2: Page 2 and 3 split.desc.4=Sənəd #2: Səhifə 2 və 3
split.desc.5=Document #3: Page 4, 5, 6 and 7 split.desc.5=Sənəd #3: Səhifə 4, 5, 6 7
split.desc.6=Document #4: Page 8 split.desc.6=Sənəd #4: Səhifə 8
split.desc.7=Document #5: Page 9 split.desc.7=Sənəd #5: Səhifə 9
split.desc.8=Document #6: Page 10 split.desc.8=Sənəd #6: Səhifə 10
split.splitPages=Enter pages to split on: split.splitPages=Bölünəcək Səhifələri Daxil Edin:
split.submit=Split split.submit=Bölün
#merge #merge
imageToPDF.title=Image to PDF imageToPDF.title=Şəkli PDF
imageToPDF.header=Image to PDF imageToPDF.header=Şəkli PDF
imageToPDF.submit=Convert imageToPDF.submit=Çevir
imageToPDF.selectLabel=Image Fit Options imageToPDF.selectLabel=Şəkil Uyğunluğu Seçimləri
imageToPDF.fillPage=Fill Page imageToPDF.fillPage=Səhifəni Doldur
imageToPDF.fitDocumentToImage=Fit Page to Image imageToPDF.fitDocumentToImage=Şəklə Uyğun Səhifə
imageToPDF.maintainAspectRatio=Maintain Aspect Ratios imageToPDF.maintainAspectRatio=Aspekt Nisbətlərini Qoruyun
imageToPDF.selectText.2=Auto rotate PDF imageToPDF.selectText.2=PDF-i Avtomatik Fırlat
imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple images) imageToPDF.selectText.3=Çoxsaylı Fayl Məntiqi (Yalnız Birdən Çox Şəkil İlə İşləyərkən Aktivdir)
imageToPDF.selectText.4=Merge into single PDF imageToPDF.selectText.4=Tək Bir PDF-ə Birləşdir
imageToPDF.selectText.5=Convert to separate PDFs imageToPDF.selectText.5=Ayrı PDF-lərə Çevirin
#pdfToImage #pdfToImage
@@ -1026,97 +1028,97 @@ pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
#addPassword #addPassword
addPassword.title=Add Password addPassword.title=Şifr Əlavə Et
addPassword.header=Add password (Encrypt) addPassword.header=Şifr Əlavə Et (Şifrləmə)
addPassword.selectText.1=Select PDF to encrypt addPassword.selectText.1=Şifrlənəcək PDF-i seç
addPassword.selectText.2=User Password addPassword.selectText.2=İstifadəçi Şifri
addPassword.selectText.3=Encryption Key Length addPassword.selectText.3=Şifrləmə Açarı Uzunluğu
addPassword.selectText.4=Higher values are stronger, but lower values have better compatibility. addPassword.selectText.4=Böyük dəyərlər daha güclüdür, lakin kiçik dəyərlərin uyğunluğu yüksəkdir.
addPassword.selectText.5=Permissions to set (Recommended to be used along with Owner password) addPassword.selectText.5=Təyin olunacaq icazə (Sahib (Owner) Şifri ilə birgə istifadə olunması tövsiyə olunur.)
addPassword.selectText.6=Prevent assembly of document addPassword.selectText.6=Sənədin strukturunun dəyişilməsinin qarşısını al
addPassword.selectText.7=Prevent content extraction addPassword.selectText.7=Məzmun xaric edilməsinin qarşısını al
addPassword.selectText.8=Prevent extraction for accessibility addPassword.selectText.8=Əlçatanlıq üçün xaricetmənin qarşısını al
addPassword.selectText.9=Prevent filling in form addPassword.selectText.9=Anketin doldurulmasının qarşısını al
addPassword.selectText.10=Prevent modification addPassword.selectText.10=Modifikasiyanın qarşısını al
addPassword.selectText.11=Prevent annotation modification addPassword.selectText.11=Sitat modifikasiyasının qarşısını al
addPassword.selectText.12=Prevent printing addPassword.selectText.12=Çap etmənin qarşısını al
addPassword.selectText.13=Prevent printing different formats addPassword.selectText.13=Müxtəlif formatların çap edilməsinin qarşısını al
addPassword.selectText.14=Owner Password addPassword.selectText.14=Sahib Şifri
addPassword.selectText.15=Restricts what can be done with the document once it is opened (Not supported by all readers) addPassword.selectText.15=Sənəd açıldıqdan sonra onunla nə edilə biləcəyini limitləndir (Bütün oxuyucular dəstəkləmir)
addPassword.selectText.16=Restricts the opening of the document itself addPassword.selectText.16=Sənədin özünün açılmağını limitləndirir
addPassword.submit=Encrypt addPassword.submit=Şifrlə
#watermark #watermark
watermark.title=Add Watermark watermark.title=Watermark Əlavə Et
watermark.header=Add Watermark watermark.header=Watermark Əlavə Et
watermark.selectText.1=Select PDF to add watermark to: watermark.selectText.1=Watermark əlavə olunacaq PDF-i seç
watermark.selectText.2=Watermark Text: watermark.selectText.2=Watermark Mətni:
watermark.selectText.3=Font Size: watermark.selectText.3=Şrift Ölçüsü:
watermark.selectText.4=Rotation (0-360): watermark.selectText.4=Fırlatma (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally): watermark.selectText.5=enBoşluq (Üfuqi olaraq watermark-lar arasındakı məsafə):
watermark.selectText.6=heightSpacer (Space between each watermark vertically): watermark.selectText.6=uzunluqBoşluq (Şaquli olaraq watermark-lar arasındakı məsafə):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Şəffaflıq (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Tipi:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Şəkili:
watermark.selectText.10=Convert PDF to PDF-Image watermark.selectText.10=PDF-i PDF-Şəkil-ə çevir
watermark.submit=Add Watermark watermark.submit=Watermark Əlavə Et
watermark.type.1=Text watermark.type.1=Mətn
watermark.type.2=Image watermark.type.2=Şəkil
#Change permissions #Change permissions
permissions.title=Change Permissions permissions.title=İcazələri Dəyişdir
permissions.header=Change Permissions permissions.header=İcazələri Dəyişdir
permissions.warning=Warning to have these permissions be unchangeable it is recommended to set them with a password via the add-password page permissions.warning=Bu İcazələrin Dəyişməz Olması İlə Bağlı Xəbərdarlıq Edərək, Onları Parol Əlavə Et Səhifəsi Vasitəsilə Parolla Təyin Etmək Tövsiyə Olunur.
permissions.selectText.1=Select PDF to change permissions permissions.selectText.1=İcazələri Dəyişdirmək Üçün PDF-i Seç
permissions.selectText.2=Permissions to set permissions.selectText.2=Tənzimlənmiş İcazələr
permissions.selectText.3=Prevent assembly of document permissions.selectText.3=Sənədin Yığılmasının Qarşısını Al
permissions.selectText.4=Prevent content extraction permissions.selectText.4=Məzmunun Çıxarılmasının Qarşısını Al
permissions.selectText.5=Prevent extraction for accessibility permissions.selectText.5=Əlçatanlıq Üçün Çıxarılmasının Qarşısını Alın
permissions.selectText.6=Prevent filling in form permissions.selectText.6=Formanın Doldurulmasının Qarşısını Alır
permissions.selectText.7=Prevent modification permissions.selectText.7=Modifikasiyanın Qarşısını Al
permissions.selectText.8=Prevent annotation modification permissions.selectText.8=Annotasiyanın Dəyişdirilməsinin Qarşısını Almaq
permissions.selectText.9=Prevent printing permissions.selectText.9=Çapın Qarşısını Al
permissions.selectText.10=Prevent printing different formats permissions.selectText.10=Fərqli Formatlarda Çapın Qarşısını Al
permissions.submit=Change permissions.submit=Dəyiş
#remove password #remove password
removePassword.title=Remove password removePassword.title=Şifri Sil
removePassword.header=Remove password (Decrypt) removePassword.header=Şifri Sil (Deşifr)
removePassword.selectText.1=Select PDF to Decrypt removePassword.selectText.1=Deşifr Üçün PDF-i Seç
removePassword.selectText.2=Password removePassword.selectText.2=Şifr
removePassword.submit=Remove removePassword.submit=Sil
#changeMetadata #changeMetadata
changeMetadata.title=Change Metadata changeMetadata.title=Metadata-nı Dəyiş
changeMetadata.header=Change Metadata changeMetadata.header=Metadata-nı Dəyiş
changeMetadata.selectText.1=Please edit the variables you wish to change changeMetadata.selectText.1=Dəyişmək istədiyiniz dəyişənləri redaktə edin
changeMetadata.selectText.2=Delete all metadata changeMetadata.selectText.2=Bütün Metadata-nı Sil
changeMetadata.selectText.3=Show Custom Metadata: changeMetadata.selectText.3=Fərdi Metadatanı göstərin:
changeMetadata.author=Author: changeMetadata.author=Müəllif:
changeMetadata.creationDate=Creation Date (yyyy/MM/dd HH:mm:ss): changeMetadata.creationDate=Yaradılma Tarixi (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Creator: changeMetadata.creator=Yaradıcı:
changeMetadata.keywords=Keywords: changeMetadata.keywords=Açar Sözlər:
changeMetadata.modDate=Modification Date (yyyy/MM/dd HH:mm:ss): changeMetadata.modDate=Dəyişiklik Tarixi (yyyy/MM/dd HH:mm:ss):
changeMetadata.producer=Producer: changeMetadata.producer=İstehsalçı:
changeMetadata.subject=Subject: changeMetadata.subject=Mövzu:
changeMetadata.trapped=Trapped: changeMetadata.trapped=Tələ:
changeMetadata.selectText.4=Other Metadata: changeMetadata.selectText.4=Digər Metadata:
changeMetadata.selectText.5=Add Custom Metadata Entry changeMetadata.selectText.5=Xüsusi Metadata girişi əlavə edin
changeMetadata.submit=Change changeMetadata.submit=Dəyiş
#pdfToPDFA #pdfToPDFA
pdfToPDFA.title=PDF To PDF/A pdfToPDFA.title=PDF-i PDF/A-ya
pdfToPDFA.header=PDF To PDF/A pdfToPDFA.header=PDF-i PDF/A-ya
pdfToPDFA.credit=This service uses qpdf for PDF/A conversion pdfToPDFA.credit=Bu Servis PDF/A Çevirmək Üçün ghostscript İşlədir
pdfToPDFA.submit=Convert pdfToPDFA.submit=Çevir
pdfToPDFA.tip=Currently does not work for multiple inputs at once pdfToPDFA.tip=Hazırda Birdən Çox Giriş Üçün İşləmir
pdfToPDFA.outputFormat=Output format pdfToPDFA.outputFormat=Çıxış Formatı
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step. pdfToPDFA.pdfWithDigitalSignature=PDF Rəqəmsal İmza Ehtiva Edir.Bu, növbəti addımda silinəcək.
#PDFToWord #PDFToWord
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Səviyyəsi: Bölmə üçün istifadə ediləcə
splitByChapters.desc.3=Metadatanı daxil edin: Əgər yoxlanılıbsa, orijinal PDF-in metadatası hər bir bölünmüş PDF-ə daxil ediləcək. splitByChapters.desc.3=Metadatanı daxil edin: Əgər yoxlanılıbsa, orijinal PDF-in metadatası hər bir bölünmüş PDF-ə daxil ediləcək.
splitByChapters.desc.4=Allow Duplicates: Dublikatlara icazə verin: Əgər işarələnərsə, eyni səhifədə birdən çox bookmarka ayrı-ayrı PDF sənədləri yaratmağa icazə verin. splitByChapters.desc.4=Allow Duplicates: Dublikatlara icazə verin: Əgər işarələnərsə, eyni səhifədə birdən çox bookmarka ayrı-ayrı PDF sənədləri yaratmağa icazə verin.
splitByChapters.submit=PDF-i Ayır splitByChapters.submit=PDF-i Ayır
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Buraxılışlar
releases.title=Buraxılış Qeydləri
releases.header=Buraxılış Qeydləri
releases.current.version=Hazırki Buraxılış
releases.note=Buraxılış Qeydləri yalnız ingiliscə mövcuddur

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Ниво на отметка: Изберете нивот
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF. splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове. splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Разделяне на PDF splitByChapters.submit=Разделяне на PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Nivell de Marcadors: Tria el nivell de marcadors que s'ut
splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit. splitByChapters.desc.3=Incloure Metadades: Si està marcat, les metadades del PDF original s'inclouran en cada PDF dividit.
splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats. splitByChapters.desc.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
splitByChapters.submit=Divideix PDF splitByChapters.submit=Divideix PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Úroveň záhlaví: Zvolte úroveň záhlaví pro použit
splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru. splitByChapters.desc.3=Zahrnout metadatů: Pokud je zaškrtnuto, původní metadata PDF souboru budou zahrnuty do každého odděleného PDF souboru.
splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně. splitByChapters.desc.4=Povolit duplicitní záznamy: Pokud je zaškrtnuto, návštěvníci mohou vytvořit samostatné PDF soubory z více záhlaví na stejné straně.
splitByChapters.submit=Podělit se PDF splitByChapters.submit=Podělit se PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bogmærke niveau: Vælg nivået af bogmærker, der skal b
splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF. splitByChapters.desc.3=Inkluder metadata: Hvis markeret, vil den originale PDF's metadata inkluderes i hver splitterdels PDF.
splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er. splitByChapters.desc.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
splitByChapters.submit=Splitter PDF splitByChapters.submit=Splitter PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=Diese Funktion ist auch auf unserer <a href="{0}">PDF-Multitool-Seite</a> verfügbar. Probieren Sie sie aus, denn sie bietet eine verbesserte Benutzeroberfläche und zusätzliche Funktionen! multiTool-advert.message=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!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Lesezeichenebene: Wählen Sie die Ebene der Lesezeichen,
splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen. splitByChapters.desc.3=Metadaten einschließen: Wenn diese Option aktiviert ist, werden die Metadaten der ursprünglichen PDF-Datei in jede aufgeteilte PDF-Datei übernommen.
splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen. splitByChapters.desc.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
splitByChapters.submit=PDF teilen splitByChapters.submit=PDF teilen
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Επιλέξτε PDF για την προσθήκη το
watermark.selectText.2=Κείμενο Υδατογραφήματος: watermark.selectText.2=Κείμενο Υδατογραφήματος:
watermark.selectText.3=Μέγεθος Κειμένου: watermark.selectText.3=Μέγεθος Κειμένου:
watermark.selectText.4=Περιστροφή (0-360): watermark.selectText.4=Περιστροφή (0-360):
watermark.selectText.5=widthSpacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια): watermark.selectText.5=Width Spacer (Κενό μεταξύ κάθε υδατογραφήματος οριζόντια):
watermark.selectText.6=heightSpacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα): watermark.selectText.6=Height Spacer (Κενό μεταξύ κάθε υδατογραφήματος κάθετα):
watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%): watermark.selectText.7=Αδιαφάνεια (Opacity) (0% - 100%):
watermark.selectText.8=Τύπος Υδατογραφήματος: watermark.selectText.8=Τύπος Υδατογραφήματος:
watermark.selectText.9=Εικόνα Υδατογραφήματος: watermark.selectText.9=Εικόνα Υδατογραφήματος:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Επίπεδο Σήμανσης Σκέψης: Επιλέ
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF. splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας. splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
splitByChapters.submit=Διαλύστε το PDF splitByChapters.submit=Διαλύστε το PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text: watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size: watermark.selectText.3=Font Size:
watermark.selectText.4=Rotation (0-360): watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally): watermark.selectText.5=Width Spacer (Space between each watermark horizontally):
watermark.selectText.6=heightSpacer (Space between each watermark vertically): watermark.selectText.6=Height Spacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
@@ -1261,6 +1263,11 @@ splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes #release notes
releases.footer=Releases releases.footer=Releases

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text: watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size: watermark.selectText.3=Font Size:
watermark.selectText.4=Rotation (0-360): watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally): watermark.selectText.5=Width Spacer (Space between each watermark horizontally):
watermark.selectText.6=heightSpacer (Space between each watermark vertically): watermark.selectText.6=Height Spacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type: watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image: watermark.selectText.9=Watermark Image:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Nivel de Marcador: Elige el nivel de marcadores para divi
splitByChapters.desc.3=Incluir Metadatos: Si está seleccionado, los metadatos del PDF original se incluirán en cada PDF dividido. splitByChapters.desc.3=Incluir Metadatos: Si está seleccionado, los metadatos del PDF original se incluirán en cada PDF dividido.
splitByChapters.desc.4=Permitir Duplicados: Si está seleccionado, permite que múltiples marcadores en la misma página creen archivos PDF separados. splitByChapters.desc.4=Permitir Duplicados: Si está seleccionado, permite que múltiples marcadores en la misma página creen archivos PDF separados.
splitByChapters.submit=Dividir PDF splitByChapters.submit=Dividir PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Déplacer vers la gauche
multiTool.moveRight=Déplacer vers la droite multiTool.moveRight=Déplacer vers la droite
multiTool.delete=Supprimer multiTool.delete=Supprimer
multiTool.dragDropMessage=Page(s) sélectionnées multiTool.dragDropMessage=Page(s) sélectionnées
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=Cette fonctionnalité est aussi disponible dans la <a href="{0}">page de l'outil multifonction</a>. Allez-y pour une interface page par page améliorée et des fonctionnalités additionnelles ! multiTool-advert.message=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 !
@@ -1054,8 +1056,8 @@ watermark.selectText.1=PDF auquel ajouter un filigrane
watermark.selectText.2=Texte du filigrane watermark.selectText.2=Texte du filigrane
watermark.selectText.3=Taille de police watermark.selectText.3=Taille de police
watermark.selectText.4=Rotation (de 0 à 360 degrés) watermark.selectText.4=Rotation (de 0 à 360 degrés)
watermark.selectText.5=widthSpacer (espace entre chaque filigrane horizontalement) watermark.selectText.5=Width Spacer (espace entre chaque filigrane horizontalement)
watermark.selectText.6=heightSpacer (espace entre chaque filigrane verticalement) watermark.selectText.6=Height Spacer (espace entre chaque filigrane verticalement)
watermark.selectText.7=Opacité (de 0% à 100%) watermark.selectText.7=Opacité (de 0% à 100%)
watermark.selectText.8=Type de filigrane watermark.selectText.8=Type de filigrane
watermark.selectText.9=Image du filigrane watermark.selectText.9=Image du filigrane
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Niveau de Signet : Choisissez le niveau de signets à uti
splitByChapters.desc.3=Inclure les Métadonnées : Si coché, les métadonnées du PDF original seront incluses dans chaque PDF divisé. splitByChapters.desc.3=Inclure les Métadonnées : Si coché, les métadonnées du PDF original seront incluses dans chaque PDF divisé.
splitByChapters.desc.4=Autoriser les Doublons : Si coché, permet à plusieurs signets sur la même page de créer des PDF séparés. splitByChapters.desc.4=Autoriser les Doublons : Si coché, permet à plusieurs signets sur la même page de créer des PDF séparés.
splitByChapters.submit=Diviser le PDF splitByChapters.submit=Diviser le PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,7 +1056,7 @@ watermark.selectText.1=Roghnaigh PDF chun comhartha uisce a chur leis:
watermark.selectText.2=Téacs Comhartha Uisce: watermark.selectText.2=Téacs Comhartha Uisce:
watermark.selectText.3=Méid cló: watermark.selectText.3=Méid cló:
watermark.selectText.4=Rothlú (0-360): watermark.selectText.4=Rothlú (0-360):
watermark.selectText.5=widthSpacer (Spás idir gach comhartha uisce go cothrománach): watermark.selectText.5=Width Spacer (Spás idir gach comhartha uisce go cothrománach):
watermark.selectText.6=spásaire airde (Spás idir gach comhartha uisce go hingearach): watermark.selectText.6=spásaire airde (Spás idir gach comhartha uisce go hingearach):
watermark.selectText.7=Teimhneacht (0% - 100%): watermark.selectText.7=Teimhneacht (0% - 100%):
watermark.selectText.8=Cineál Comhartha Uisce: watermark.selectText.8=Cineál Comhartha Uisce:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=लेमैक्स स्तर: विभाजन
splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा। splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा।
splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है। splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है।
splitByChapters.submit=PDF विभाजित splitByChapters.submit=PDF विभाजित
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Nivo oznaka: Odaberite nivo oznaka koji će se koristiti
splitByChapters.desc.3=Uključi metapodatke: Ako je pokušano, metapodaci iz originalne PDF datoteke će biti uključeni u svaku podijeljenu PDF datoteku. splitByChapters.desc.3=Uključi metapodatke: Ako je pokušano, metapodaci iz originalne PDF datoteke će biti uključeni u svaku podijeljenu PDF datoteku.
splitByChapters.desc.4=Dopuštaj duplikate: Ako je ova opcija zaštićena, dozvoljava se da se na istoj strani mogu stvoriti posebne PDF datoteke s više oznaka. splitByChapters.desc.4=Dopuštaj duplikate: Ako je ova opcija zaštićena, dozvoljava se da se na istoj strani mogu stvoriti posebne PDF datoteke s više oznaka.
splitByChapters.submit=Podijeli PDF splitByChapters.submit=Podijeli PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Válassza ki a PDF-t, amelyhez vízjelet kíván hozzáad
watermark.selectText.2=Vízjel szövege: watermark.selectText.2=Vízjel szövege:
watermark.selectText.3=Betűméret: watermark.selectText.3=Betűméret:
watermark.selectText.4=Forgatás (0-360): watermark.selectText.4=Forgatás (0-360):
watermark.selectText.5=widthSpacer (Hely a vízjelek között vízszintesen): watermark.selectText.5=Width Spacer (Hely a vízjelek között vízszintesen):
watermark.selectText.6=heightSpacer (Hely a vízjelek között függőlegesen): watermark.selectText.6=Height Spacer (Hely a vízjelek között függőlegesen):
watermark.selectText.7=Átlátszóság (0% - 100%): watermark.selectText.7=Átlátszóság (0% - 100%):
watermark.selectText.8=Vízjel típusa: watermark.selectText.8=Vízjel típusa:
watermark.selectText.9=Vízjel képe: watermark.selectText.9=Vízjel képe:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Metaadatok belefoglalása: Ha bevanítva van, az eredeti PDF fájl metaadatai megtartódnak minden osztott fájlban. splitByChapters.desc.3=Metaadatok belefoglalása: Ha bevanítva van, az eredeti PDF fájl metaadatai megtartódnak minden osztott fájlban.
splitByChapters.desc.4=Duplikációk engedélyezése: Ha bevanítva van, lehetővé teszi a megadott oldalon lévő több kijelzőszint alapján új PDF-ek létrehozása. splitByChapters.desc.4=Duplikációk engedélyezése: Ha bevanítva van, lehetővé teszi a megadott oldalon lévő több kijelzőszint alapján új PDF-ek létrehozása.
splitByChapters.submit=PDF osztás splitByChapters.submit=PDF osztás
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Pilih PDF untuk menambahkan watermark:
watermark.selectText.2=Text Watermark: watermark.selectText.2=Text Watermark:
watermark.selectText.3=Ukuran Huruf: watermark.selectText.3=Ukuran Huruf:
watermark.selectText.4=Rotasi (0-360): watermark.selectText.4=Rotasi (0-360):
watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal): watermark.selectText.5=Width Spacer (Spasi diantara setiap watermark horisontal):
watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal): watermark.selectText.6=Height Spacer (Spasi diantara setiap watermark vertikal):
watermark.selectText.7=Kejernihan (0% - 100%): watermark.selectText.7=Kejernihan (0% - 100%):
watermark.selectText.8=Tipe Watermark: watermark.selectText.8=Tipe Watermark:
watermark.selectText.9=Gambar Watermark: watermark.selectText.9=Gambar Watermark:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Tingkatan Markah: Pilih tingkatan markah yang digunakan u
splitByChapters.desc.3=Termasuk Metadata: Jika dicentang, metadata asli PDF akan disertakan dalam setiap PDF yang dibagi. splitByChapters.desc.3=Termasuk Metadata: Jika dicentang, metadata asli PDF akan disertakan dalam setiap PDF yang dibagi.
splitByChapters.desc.4=Izinkan Duplikat: Jika dicentang, mengizinkan beberapa markah pada halaman yang sama untuk membuat PDF terpisah. splitByChapters.desc.4=Izinkan Duplikat: Jika dicentang, mengizinkan beberapa markah pada halaman yang sama untuk membuat PDF terpisah.
splitByChapters.submit=Pecah PDF splitByChapters.submit=Pecah PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Sposta a sinistra
multiTool.moveRight=Sposta a destra multiTool.moveRight=Sposta a destra
multiTool.delete=Elimina multiTool.delete=Elimina
multiTool.dragDropMessage=Pagina(e) selezionata(e) multiTool.dragDropMessage=Pagina(e) selezionata(e)
multiTool.undo=Annulla
multiTool.redo=Rifai
#multiTool-advert #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! 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!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Livello segnalibro: seleziona il livello dei segnalibri d
splitByChapters.desc.3=Includi metadati: se selezionato, i metadati del PDF originale verranno inclusi in ogni PDF diviso. splitByChapters.desc.3=Includi metadati: se selezionato, i metadati del PDF originale verranno inclusi in ogni PDF diviso.
splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati. splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
splitByChapters.submit=Dividi PDF splitByChapters.submit=Dividi PDF
#File Chooser
fileChooser.click=Clicca
fileChooser.or=o
fileChooser.dragAndDrop=Trascina & Rilascia
fileChooser.hoveredDragAndDrop=Trascina & rilascia i file qui
#release notes
releases.footer=Rilasci
releases.title=Note di rilascio
releases.header=Note di rilascio
releases.current.version=Rilascio corrente
releases.note=Le note di rilascio sono disponibili solo in inglese

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=북마크 레벨: 분할에 사용할 북마크 레벨을
splitByChapters.desc.3=메타데이터 포함: 체크하면 각 분할된 PDF에는 원본 PDF의 메타데이터가 포함됩니다. splitByChapters.desc.3=메타데이터 포함: 체크하면 각 분할된 PDF에는 원본 PDF의 메타데이터가 포함됩니다.
splitByChapters.desc.4=중복 허용: 중복 북마크가 있는 같은 페이지에 여러 번 분할 PDF를 생성합니다. splitByChapters.desc.4=중복 허용: 중복 북마크가 있는 같은 페이지에 여러 번 분할 PDF를 생성합니다.
splitByChapters.submit=PDF 분할 splitByChapters.submit=PDF 분할
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Boekmarkeer niveau: Kies het boekmarkeer niveau om te geb
splitByChapters.desc.3=Metadata inclusief: Als gecijfeld, de originele PDF's metadata wordt ingevoegd in elk gesplitst PDF-bestand. splitByChapters.desc.3=Metadata inclusief: Als gecijfeld, de originele PDF's metadata wordt ingevoegd in elk gesplitst PDF-bestand.
splitByChapters.desc.4=Dubbele items toestaan: Als gecijfeld, zorgen multiple boekmarkeersymboolen op dezelfde pagina voor het maken van aparte PDF-bestanden. splitByChapters.desc.4=Dubbele items toestaan: Als gecijfeld, zorgen multiple boekmarkeersymboolen op dezelfde pagina voor het maken van aparte PDF-bestanden.
splitByChapters.submit=PDF splitsen splitByChapters.submit=PDF splitsen
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Poziom Zakładek: Wybierz poziom zakładek, który ma zos
splitByChapters.desc.3=Dołącz Metadane: Jeśli opcja ta jest zaznaczona, metadane oryginalnego pliku PDF zostaną uwzględnione w każdym rozdzielonych plików PDF. splitByChapters.desc.3=Dołącz Metadane: Jeśli opcja ta jest zaznaczona, metadane oryginalnego pliku PDF zostaną uwzględnione w każdym rozdzielonych plików PDF.
splitByChapters.desc.4=Zezwól na Duplikaty: Jeśli ta opcja jest zaznaczona, pozwala na tworzenie oddzielnych plików PDF przez wiele zakładek na tej samej stronie. splitByChapters.desc.4=Zezwól na Duplikaty: Jeśli ta opcja jest zaznaczona, pozwala na tworzenie oddzielnych plików PDF przez wiele zakładek na tej samej stronie.
splitByChapters.submit=Podziel PDF splitByChapters.submit=Podziel PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Selecione PDF para adicionar a marca d'água:
watermark.selectText.2=Texto da marca d'água: watermark.selectText.2=Texto da marca d'água:
watermark.selectText.3=Tamanho da fonte: watermark.selectText.3=Tamanho da fonte:
watermark.selectText.4=Rotação (0-360): watermark.selectText.4=Rotação (0-360):
watermark.selectText.5=widthSpacer (Espaço entre cada marca d'água horizontalmente): watermark.selectText.5=Width Spacer (Espaço entre cada marca d'água horizontalmente):
watermark.selectText.6=heightSpacer (Espaço entre cada marca d'água verticalmente): watermark.selectText.6=Height Spacer (Espaço entre cada marca d'água verticalmente):
watermark.selectText.7=Opacidade (0% - 100%): watermark.selectText.7=Opacidade (0% - 100%):
watermark.selectText.8=Tipo de marca d'água: watermark.selectText.8=Tipo de marca d'água:
watermark.selectText.9=Imagem da marca d'água: watermark.selectText.9=Imagem da marca d'água:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Nível de Marcador: Escolha o nível de marcador a ser us
splitByChapters.desc.3=Incluir Metadados: Se marcado, os metadados do PDF original serão incluidos em cada divisão do PDF. splitByChapters.desc.3=Incluir Metadados: Se marcado, os metadados do PDF original serão incluidos em cada divisão do PDF.
splitByChapters.desc.4=Permitir Cópias: Se marcado, habilita vários marcadores na mesma página para criar PDFs separados. splitByChapters.desc.4=Permitir Cópias: Se marcado, habilita vários marcadores na mesma página para criar PDFs separados.
splitByChapters.submit=Dividir PDF splitByChapters.submit=Dividir PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Seleccione o PDF para Adicionar a Marca d'Água
watermark.selectText.2=Texto da Marca d'Água watermark.selectText.2=Texto da Marca d'Água
watermark.selectText.3=Tamanho da Fonte watermark.selectText.3=Tamanho da Fonte
watermark.selectText.4=Rotação (0-360) watermark.selectText.4=Rotação (0-360)
watermark.selectText.5=Espaçamento Horizontal (widthSpacer) watermark.selectText.5=Espaçamento Horizontal (Width Spacer)
watermark.selectText.6=Espaçamento Vertical (heightSpacer) watermark.selectText.6=Espaçamento Vertical (Height Spacer)
watermark.selectText.7=Opacidade (0% - 100%) watermark.selectText.7=Opacidade (0% - 100%)
watermark.selectText.8=Tipo de Marca d'Água watermark.selectText.8=Tipo de Marca d'Água
watermark.selectText.9=Imagem da Marca d'Água watermark.selectText.9=Imagem da Marca d'Água
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Nível de Anotações: Escolha o nível das anotações a
splitByChapters.desc.3=Inclua Metadados: Se marcado, os metadados originais do PDF serão incluídos em cada PDF dividido. splitByChapters.desc.3=Inclua Metadados: Se marcado, os metadados originais do PDF serão incluídos em cada PDF dividido.
splitByChapters.desc.4=Permitir Duplicatas: Se marcado, permite a criação de vários bookmarks na mesma página para criar separadamente vários PDFs. splitByChapters.desc.4=Permitir Duplicatas: Se marcado, permite a criação de vários bookmarks na mesma página para criar separadamente vários PDFs.
splitByChapters.submit=Dividir o PDF splitByChapters.submit=Dividir o PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Выберите PDF, чтобы добавить вод
watermark.selectText.2=Текст водяного знака: watermark.selectText.2=Текст водяного знака:
watermark.selectText.3=Размер шрифта: watermark.selectText.3=Размер шрифта:
watermark.selectText.4=Поворот (0-360): watermark.selectText.4=Поворот (0-360):
watermark.selectText.5=widthSpacer (пробел между каждым водяным знаком по горизонтали): watermark.selectText.5=Width Spacer (пробел между каждым водяным знаком по горизонтали):
watermark.selectText.6=heightSpacer (пробел между каждым водяным знаком по вертикали): watermark.selectText.6=Height Spacer (пробел между каждым водяным знаком по вертикали):
watermark.selectText.7=Непрозрачность (0% - 100%): watermark.selectText.7=Непрозрачность (0% - 100%):
watermark.selectText.8=Тип водяного знака: watermark.selectText.8=Тип водяного знака:
watermark.selectText.9=Изображение водяного знака: watermark.selectText.9=Изображение водяного знака:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Уровень закладки: выберите уро
splitByChapters.desc.3=Включить метаданные: если эта опция отмечена, метаданные исходного PDF будут включены в каждый разбитый PDF. splitByChapters.desc.3=Включить метаданные: если эта опция отмечена, метаданные исходного PDF будут включены в каждый разбитый PDF.
splitByChapters.desc.4=Позволять дубликаты: если эта опция отмечена, на одной странице могут быть созданы несколько PDF из-за нескольких одинаковых закладок. splitByChapters.desc.4=Позволять дубликаты: если эта опция отмечена, на одной странице могут быть созданы несколько PDF из-за нескольких одинаковых закладок.
splitByChapters.submit=Разделить PDF splitByChapters.submit=Разделить PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Välj PDF för att lägga till vattenstämpel till:
watermark.selectText.2=Vattenmärkestext: watermark.selectText.2=Vattenmärkestext:
watermark.selectText.3=Teckenstorlek: watermark.selectText.3=Teckenstorlek:
watermark.selectText.4=Vändning (0-360): watermark.selectText.4=Vändning (0-360):
watermark.selectText.5=widthSpacer (mellanrum mellan varje vattenstämpel horisontellt): watermark.selectText.5=Width Spacer (mellanrum mellan varje vattenstämpel horisontellt):
watermark.selectText.6=heightSpacer (mellanrum mellan varje vattenstämpel vertikalt): watermark.selectText.6=Height Spacer (mellanrum mellan varje vattenstämpel vertikalt):
watermark.selectText.7=Opacitet (0% - 100%): watermark.selectText.7=Opacitet (0% - 100%):
watermark.selectText.8=Vattenstämpeltyp: watermark.selectText.8=Vattenstämpeltyp:
watermark.selectText.9=Vattenstämpelbild: watermark.selectText.9=Vattenstämpelbild:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bokmärkesnivå: Välj nivån av bokmärken att använda
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Tillåt duplicieringar: Om kryssrutan är markerad tillåts flera bokmärken på samma sida skapa individuella PDF:er. splitByChapters.desc.4=Tillåt duplicieringar: Om kryssrutan är markerad tillåts flera bokmärken på samma sida skapa individuella PDF:er.
splitByChapters.submit=Dela upp PDF splitByChapters.submit=Dela upp PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=ระดับบุคคลที่ได้รับ
splitByChapters.desc.3=รวมข้อมูลเสริม: หากถูกเลือก ข้อมูลเสริมของไฟล์ PDF ที่เดิมจะถูกรวมอยู่ในแต่ละไฟล์ที่แบ่งออก splitByChapters.desc.3=รวมข้อมูลเสริม: หากถูกเลือก ข้อมูลเสริมของไฟล์ PDF ที่เดิมจะถูกรวมอยู่ในแต่ละไฟล์ที่แบ่งออก
splitByChapters.desc.4=อนุญาตให้มีการซ้ำ: หากถูกเลือก จะทำให้สามารถสร้างไฟล์ PDF แยกออกมาจากหน้าเดียวกันได้หลายรายการ splitByChapters.desc.4=อนุญาตให้มีการซ้ำ: หากถูกเลือก จะทำให้สามารถสร้างไฟล์ PDF แยกออกมาจากหน้าเดียวกันได้หลายรายการ
splitByChapters.submit=แบ่งไฟล์ PDF splitByChapters.submit=แบ่งไฟล์ PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=Виберіть PDF, щоб додати водяний
watermark.selectText.2=Текст водяного знаку: watermark.selectText.2=Текст водяного знаку:
watermark.selectText.3=Розмір шрифту: watermark.selectText.3=Розмір шрифту:
watermark.selectText.4=Обертання (0-360): watermark.selectText.4=Обертання (0-360):
watermark.selectText.5=widthSpacer (проміжок між кожним водяним знаком по горизонталі): watermark.selectText.5=Width Spacer (проміжок між кожним водяним знаком по горизонталі):
watermark.selectText.6=heightSpacer (проміжок між кожним водяним знаком по вертикалі): watermark.selectText.6=Height Spacer (проміжок між кожним водяним знаком по вертикалі):
watermark.selectText.7=Непрозорість (0% - 100%): watermark.selectText.7=Непрозорість (0% - 100%):
watermark.selectText.8=Тип водяного знаку: watermark.selectText.8=Тип водяного знаку:
watermark.selectText.9=Зображення водяного знаку: watermark.selectText.9=Зображення водяного знаку:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF. splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs. splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF splitByChapters.submit=Split PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right multiTool.moveRight=Move Right
multiTool.delete=Delete multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#multiTool-advert #multiTool-advert
multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
@@ -1054,8 +1056,8 @@ watermark.selectText.1=選擇要新增浮水印的 PDF
watermark.selectText.2=浮水印文字: watermark.selectText.2=浮水印文字:
watermark.selectText.3=字型大小: watermark.selectText.3=字型大小:
watermark.selectText.4=旋轉0-360 watermark.selectText.4=旋轉0-360
watermark.selectText.5=widthSpacer每個浮水印之間的水平間距 watermark.selectText.5=Width Spacer每個浮水印之間的水平間距
watermark.selectText.6=heightSpacer每個浮水印之間的垂直間距 watermark.selectText.6=Height Spacer每個浮水印之間的垂直間距
watermark.selectText.7=不透明度0% - 100% watermark.selectText.7=不透明度0% - 100%
watermark.selectText.8=浮水印類型: watermark.selectText.8=浮水印類型:
watermark.selectText.9=浮水印影像: watermark.selectText.9=浮水印影像:
@@ -1260,3 +1262,16 @@ splitByChapters.desc.2=書籤層級選擇用於分割的書籤層級0 表
splitByChapters.desc.3=包含中繼資料:如果勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。 splitByChapters.desc.3=包含中繼資料:如果勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。
splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多個書籤建立獨立的 PDF。 splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多個書籤建立獨立的 PDF。
splitByChapters.submit=分割 PDF splitByChapters.submit=分割 PDF
#File Chooser
fileChooser.click=Click
fileChooser.or=or
fileChooser.dragAndDrop=Drag & Drop
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
#release notes
releases.footer=Releases
releases.title=Release Notes
releases.header=Release Notes
releases.current.version=Current Release
releases.note=Release notes are only available in English

View File

@@ -16,7 +16,7 @@ security:
csrfDisabled: true # set to 'true' to disable CSRF protection (not recommended for production) csrfDisabled: true # 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 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 loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # 'all' (Login Username/Password and OAuth2[must be enabled and configured]), 'normal'(only Login with Username/Password) or 'oauth2'(only Login with OAuth2) loginMethod: all # Accepts values like 'all' and 'normal'(only Login with Username/Password), 'oauth2'(only Login with OAuth2) or 'saml2'(only Login with SAML2)
initialLogin: initialLogin:
username: '' # initial username for the first login username: '' # initial username for the first login
password: '' # initial password for the first login password: '' # initial password for the first login
@@ -42,14 +42,14 @@ security:
issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint issuer: '' # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) endpoint
clientId: '' # client ID from your provider clientId: '' # client ID from your provider
clientSecret: '' # client secret from your provider clientSecret: '' # client secret from your provider
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
useAsUsername: email # default is 'email'; custom fields can be used as the username useAsUsername: email # default is 'email'; custom fields can be used as the username
scopes: openid, profile, email # specify the scopes for which the application will request permissions scopes: openid, profile, email # specify the scopes for which the application will request permissions
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak' provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2: saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users autoCreateUser: true # set to 'true' to allow auto-creation of non-existing users
blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin
registrationId: stirling registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata

View File

@@ -407,7 +407,7 @@
{ {
"moduleName": "commons-io:commons-io", "moduleName": "commons-io:commons-io",
"moduleUrl": "https://commons.apache.org/proper/commons-io/", "moduleUrl": "https://commons.apache.org/proper/commons-io/",
"moduleVersion": "2.17.0", "moduleVersion": "2.18.0",
"moduleLicense": "Apache-2.0", "moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
}, },
@@ -1386,6 +1386,13 @@
"moduleLicense": "Apache License, Version 2.0", "moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
}, },
{
"moduleName": "org.springframework.session:spring-session-core",
"moduleUrl": "https://spring.io/projects/spring-session",
"moduleVersion": "3.4.0",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{ {
"moduleName": "org.springframework:spring-aop", "moduleName": "org.springframework:spring-aop",
"moduleUrl": "https://github.com/spring-projects/spring-framework", "moduleUrl": "https://github.com/spring-projects/spring-framework",

View File

@@ -1,10 +1,221 @@
.custom-file-chooser {
display: flex;
flex-direction: column;
position: relative;
min-height: 55px;
border-radius: 1rem;
--selected-files-display: none;
}
.input-container {
position: relative;
border-radius: 1rem;
border: 1px dashed rgb(105, 116, 134);
column-gap: 7px;
row-gap: 7px;
height: 150px;
width: 100%;
--overlay-display: none;
transition: background-color 0.5s linear;
}
.input-container:hover {
outline: none;
border: none;
background-color: var(--md-sys-color-surface-container-low);
-webkit-transition: box-shadow 1s ease, background-color 2s linear;
-moz-transition: box-shadow 1s ease, background-color 2s linear;
-o-transition: box-shadow 1s ease, background-color 2s linear;
-ms-transition: box-shadow 1s ease, background-color 2s linear;
transition: box-shadow 1s ease, background-color 2s linear;
box-shadow: 0 0 10px rgb(105, 116, 134);
cursor: pointer;
}
.input-container * {
user-select: none;
pointer-events: none;
}
.input-container::before {
display: var(--overlay-display);
position: absolute;
content: '';
top: 0;
left: 0;
height: 100%;
width: 100%;
background-color: var(--md-sys-color-surface);
z-index: 1;
white-space: pre;
border-radius: 1rem;
}
.input-container::after {
display: var(--overlay-display);
position: absolute;
content: attr(data-text);
font-size: 0.9rem;
font-weight: 550;
color: var(--md-sys-color-on-surface);
background-color: transparent;
min-width: 150px;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
text-align: center;
z-index: 2;
}
input[type="file"] {
display: none;
}
.input-container div:nth-of-type(2) {
color: var(--md-sys-color-on-surface);
}
.input-container div:nth-of-type(1), .input-container div:nth-of-type(3) {
color: var(--md-sys-color-on-surface);
font-size: 16px;
font-weight: bold;
}
.file-input-btn {
display: inline-block;
border: 1px solid #ccc;
padding: 6px 12px;
cursor: pointer;
color: #212529;
font-size: 1rem;
border-radius: 3rem;
background-color: #DDE0E3;
}
.small-file-container {
padding-top: 1px;
position: relative;
row-gap: 1px;
height: 60px;
width: 60px;
}
.file-icon {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 30px;
}
.file-icon * {
height: inherit;
width: inherit;
}
.file-info {
min-width: 0;
}
.file-info > div:nth-child(1) {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--md-sys-color-on-surface);
max-width: 60px;
font-size: 0.75rem;
}
.file-info > div:nth-child(2) {
overflow: hidden;
text-overflow: ellipsis;
color: grey;
max-width: 60px;
font-size: 10px;
}
.remove-selected-file {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
height: 15px;
width: 15px;
right: 10px;
top: -5px;
}
.remove-selected-file * {
overflow: hidden;
height: inherit;
width: inherit;
z-index: 3;
pointer-events: none;
user-select: none;
}
.remove-selected-file:after {
content: '';
position: absolute;
left: 1;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: white;
z-index: 2;
user-select: none;
pointer-events: none;
}
.remove-selected-file:hover {
cursor: pointer;
}
.custom-file-label { .custom-file-label {
padding-right: 90px; padding-right: 90px;
} }
.selected-files { .selected-files {
margin-top: 10px; display: var(--selected-files-display);
max-height: 150px; padding-left: 5px;
overflow-y: auto; padding-right: 3px;
padding-top: 15px;
padding-bottom: 15px;
flex: 1;
white-space: pre-wrap; white-space: pre-wrap;
row-gap: 12px;
column-gap: 5px;
border-radius: 1rem;
border: 1px solid rgb(105, 116, 134, 0.5);
} }

View File

@@ -0,0 +1,52 @@
class FileIconFactory {
static createFileIcon(fileExtension) {
let ext = fileExtension.toLowerCase();
switch (ext) {
case "pdf":
return this.createPDFIcon();
case "csv":
return this.createCSVIcon();
case "jpe":
case "jpg":
case "jpeg":
case "gif":
case "png":
case "bmp":
case "ico":
case "svg":
case "svgz":
case "tif":
case "tiff":
case "ai":
case "drw":
case "pct":
case "psp":
case "xcf":
case "psd":
case "raw":
case "webp":
case "heic":
return this.createImageIcon();
default:
return this.createUnknownFileIcon();
}
}
static createPDFIcon() {
return `
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-filetype-pdf" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM1.6 11.85H0v3.999h.791v-1.342h.803q.43 0 .732-.173.305-.175.463-.474a1.4 1.4 0 0 0 .161-.677q0-.375-.158-.677a1.2 1.2 0 0 0-.46-.477q-.3-.18-.732-.179m.545 1.333a.8.8 0 0 1-.085.38.57.57 0 0 1-.238.241.8.8 0 0 1-.375.082H.788V12.48h.66q.327 0 .512.181.185.183.185.522m1.217-1.333v3.999h1.46q.602 0 .998-.237a1.45 1.45 0 0 0 .595-.689q.196-.45.196-1.084 0-.63-.196-1.075a1.43 1.43 0 0 0-.589-.68q-.396-.234-1.005-.234zm.791.645h.563q.371 0 .609.152a.9.9 0 0 1 .354.454q.118.302.118.753a2.3 2.3 0 0 1-.068.592 1.1 1.1 0 0 1-.196.422.8.8 0 0 1-.334.252 1.3 1.3 0 0 1-.483.082h-.563zm3.743 1.763v1.591h-.79V11.85h2.548v.653H7.896v1.117h1.606v.638z"/>
</svg>
`;
}
static createImageIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M216-144q-30 0-51-21.5T144-216v-528q0-29 21-50.5t51-21.5h528q30 0 51 21.5t21 50.5v528q0 29-21 50.5T744-144H216Zm48-144h432L552-480 444-336l-72-96-108 144Z"/></svg>`;
}
static createUnknownFileIcon() {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M263.72-96Q234-96 213-117.15T192-168v-624q0-29.7 21.15-50.85Q234.3-864 264-864h312l192 192v504q0 29.7-21.16 50.85Q725.68-96 695.96-96H263.72ZM528-624h168L528-792v168Z"/></svg>`;
}
}
export default FileIconFactory;

View File

@@ -0,0 +1,31 @@
class FileUtils {
static extractFileExtension(filename) {
if (!filename || filename.trim().length <= 0) return "";
let trimmedName = filename.trim();
return trimmedName.substring(trimmedName.lastIndexOf(".") + 1);
}
static transformFileSize(size) {
if (!size) return `0Bs`;
let oneKB = 1024;
let oneMB = oneKB * 1024;
let oneGB = oneMB * 1024;
let oneTB = oneGB * 1024;
if (size < oneKB) return `${this._toFixed(size)}Bs`;
else if (oneKB <= size && size < oneMB) return `${this._toFixed(size / oneKB)}KBs`;
else if (oneMB <= size && size < oneGB) return `${this._toFixed(size / oneMB)}MBs`;
else if (oneGB <= size && size < oneTB) return `${this._toFixed(size / oneGB)}GBs`;
else return `${this._toFixed(size / oneTB)}TBs`;
}
static _toFixed(val, digits = 1) {
// Return value without ending 0s after decimal point
// Example: if res == 145.0 then return 145, else if 145.x (where x != 0) return 145.x
let res = val.toFixed(digits);
let resRounded = (res|0);
return res == resRounded ? resRounded : res;
}
}
export default FileUtils;

View File

@@ -1,72 +1,92 @@
document.addEventListener("DOMContentLoaded", function () { import FileIconFactory from "./file-icon-factory.js";
document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput); import FileUtils from "./file-utils.js";
}); import UUID from './uuid.js';
let isScriptExecuted = false;
if (!isScriptExecuted) {
isScriptExecuted = true;
document.addEventListener("DOMContentLoaded", function () {
document.querySelectorAll(".custom-file-chooser").forEach(setupFileInput);
});
}
function setupFileInput(chooser) { function setupFileInput(chooser) {
const elementId = chooser.getAttribute("data-bs-element-id"); const elementId = chooser.getAttribute("data-bs-element-id");
const filesSelected = chooser.getAttribute("data-bs-files-selected"); const filesSelected = chooser.getAttribute("data-bs-files-selected");
const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt"); const pdfPrompt = chooser.getAttribute("data-bs-pdf-prompt");
const inputContainerId = chooser.getAttribute('data-bs-element-container-id');
let inputContainer = document.getElementById(inputContainerId);
let allFiles = []; let allFiles = [];
let overlay; let overlay;
let dragCounter = 0; let dragCounter = 0;
inputContainer.addEventListener('click', (e) => {
let inputBtn = document.getElementById(elementId);
inputBtn.click();
})
const dragenterListener = function () { const dragenterListener = function () {
dragCounter++; dragCounter++;
if (!overlay) { if (!overlay) {
overlay = document.createElement("div"); // Show overlay by removing display: none from pseudo elements (::before and ::after)
overlay.style.position = "fixed"; inputContainer.style.setProperty('--overlay-display', "''");
overlay.style.top = 0; overlay = true;
overlay.style.left = 0;
overlay.style.width = "100%";
overlay.style.height = "100%";
overlay.style.background = "rgba(0, 0, 0, 0.5)";
overlay.style.color = "#fff";
overlay.style.zIndex = "1000";
overlay.style.display = "flex";
overlay.style.alignItems = "center";
overlay.style.justifyContent = "center";
overlay.style.pointerEvents = "none";
overlay.innerHTML = "<p>Drop files anywhere to upload</p>";
document.getElementById("content-wrap").appendChild(overlay);
} }
}; };
const dragleaveListener = function () { const dragleaveListener = function () {
dragCounter--; dragCounter--;
if (dragCounter === 0) { if (dragCounter === 0) {
if (overlay) { hideOverlay();
overlay.remove();
overlay = null;
}
} }
}; };
function hideOverlay() {
if (!overlay) return;
inputContainer.style.setProperty('--overlay-display', 'none');
overlay = false;
}
const dropListener = function (e) { const dropListener = function (e) {
e.preventDefault(); e.preventDefault();
// Drag and Drop shall only affect the target file chooser
if (e.target !== inputContainer) {
hideOverlay();
dragCounter = 0;
return;
}
const dt = e.dataTransfer; const dt = e.dataTransfer;
const files = dt.files; const files = dt.files;
for (let i = 0; i < files.length; i++) { const fileInput = document.getElementById(elementId);
allFiles.push(files[i]); if (fileInput?.hasAttribute("multiple")) {
pushFileListTo(files, allFiles);
} else if (fileInput) {
allFiles = [files[0]];
} }
const dataTransfer = new DataTransfer(); const dataTransfer = new DataTransfer();
allFiles.forEach((file) => dataTransfer.items.add(file)); allFiles.forEach((file) => dataTransfer.items.add(file));
const fileInput = document.getElementById(elementId);
fileInput.files = dataTransfer.files; fileInput.files = dataTransfer.files;
if (overlay) { hideOverlay();
overlay.remove();
overlay = null;
}
dragCounter = 0; dragCounter = 0;
fileInput.dispatchEvent(new Event("change", { bubbles: true })); fileInput.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: {source: 'drag-drop'} }));
}; };
function pushFileListTo(fileList, container) {
for (let file of fileList) {
container.push(file);
}
}
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => { ["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
document.body.addEventListener(eventName, preventDefaults, false); document.body.addEventListener(eventName, preventDefaults, false);
}); });
@@ -81,32 +101,132 @@ function setupFileInput(chooser) {
document.body.addEventListener("drop", dropListener); document.body.addEventListener("drop", dropListener);
$("#" + elementId).on("change", function (e) { $("#" + elementId).on("change", function (e) {
allFiles = Array.from(e.target.files); let element = e.target;
const isDragAndDrop = e.detail?.source == 'drag-drop';
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;
});
if (!isDragAndDrop) {
let dataTransfer = toDataTransfer(allFiles);
element.files = dataTransfer.files;
}
handleFileInputChange(this); handleFileInputChange(this);
}); this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
});
function toDataTransfer(files) {
let dataTransfer = new DataTransfer();
files.forEach(file => dataTransfer.items.add(file));
return dataTransfer;
}
function handleFileInputChange(inputElement) { function handleFileInputChange(inputElement) {
const files = allFiles; const files = allFiles;
const fileNames = files.map((f) => f.name); showOrHideSelectedFilesContainer(files);
const selectedFilesContainer = $(inputElement).siblings(".selected-files");
const filesInfo = files.map((f) => ({name: f.name, size: f.size, uniqueId: f.uniqueId}));
const selectedFilesContainer = $(inputContainer).siblings(".selected-files");
selectedFilesContainer.empty(); selectedFilesContainer.empty();
fileNames.forEach((fileName) => { filesInfo.forEach((info) => {
selectedFilesContainer.append("<div>" + fileName + "</div>"); let fileContainerClasses = 'small-file-container d-flex flex-column justify-content-center align-items-center';
let fileContainer = document.createElement('div');
$(fileContainer).addClass(fileContainerClasses);
$(fileContainer).attr('id', info.uniqueId);
let fileIconContainer = createFileIconContainer(info);
let fileInfoContainer = createFileInfoContainer(info);
let removeBtn = document.createElement('div');
removeBtn.classList.add('remove-selected-file');
let removeBtnIconHTML = `<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px" fill="#C02223"><path d="m339-288 141-141 141 141 51-51-141-141 141-141-51-51-141 141-141-141-51 51 141 141-141 141 51 51ZM480-96q-79 0-149-30t-122.5-82.5Q156-261 126-331T96-480q0-80 30-149.5t82.5-122Q261-804 331-834t149-30q80 0 149.5 30t122 82.5Q804-699 834-629.5T864-480q0 79-30 149t-82.5 122.5Q699-156 629.5-126T480-96Z"/></svg>`;
$(removeBtn).append(removeBtnIconHTML);
$(removeBtn).attr('data-file-id', info.uniqueId).click(removeFileListener);
$(fileContainer).append(fileIconContainer);
$(fileContainer).append(fileInfoContainer);
$(fileContainer).append(removeBtn);
selectedFilesContainer.append(fileContainer);
}); });
if (fileNames.length === 1) {
$(inputElement).siblings(".custom-file-label").addClass("selected").html(fileNames[0]); showOrHideSelectedFilesContainer(filesInfo);
} else if (fileNames.length > 1) {
$(inputElement)
.siblings(".custom-file-label")
.addClass("selected")
.html(fileNames.length + " " + filesSelected);
} else {
$(inputElement).siblings(".custom-file-label").addClass("selected").html(pdfPrompt);
}
} }
function showOrHideSelectedFilesContainer(files) {
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');
let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);
showOrHideSelectedFilesContainer(allFiles);
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);
let dataTransfer = toDataTransfer(allFiles);
if (inputElement) inputElement.files = dataTransfer.files;
}
function createFileIconContainer(info) {
let fileIconContainer = document.createElement('div');
fileIconContainer.classList.add('file-icon');
// Add icon based on the extension
let fileExtension = FileUtils.extractFileExtension(info.name);
let fileIcon = FileIconFactory.createFileIcon(fileExtension);
$(fileIconContainer).append(fileIcon);
return fileIconContainer;
}
function createFileInfoContainer(info) {
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>`
);
let fileSizeWithUnits = FileUtils.transformFileSize(info.size);
$(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 //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 fileName = e.detail; const fileId = e.detail;
allFiles = allFiles.filter(file => file.name !== fileName); let inputElement = document.getElementById(elementId);
removeFileById(fileId, inputElement);
showOrHideSelectedFilesContainer(allFiles);
}); });
} }

View File

@@ -3,7 +3,7 @@ let currentSort = {
descending: false, descending: false,
}; };
document.getElementById("fileInput-input").addEventListener("change", function () { document.getElementById("fileInput-input").addEventListener("file-input-change", function () {
var files = this.files; var files = this.files;
displayFiles(files); displayFiles(files);
}); });
@@ -29,6 +29,7 @@ async function displayFiles(files) {
// Create filename div and set textContent to sanitize // Create filename div and set textContent to sanitize
const fileNameDiv = document.createElement("div"); const fileNameDiv = document.createElement("div");
fileNameDiv.className = "filename"; fileNameDiv.className = "filename";
fileNameDiv.setAttribute("data-file-id", files[i].uniqueId);
fileNameDiv.textContent = files[i].name; fileNameDiv.textContent = files[i].name;
// Create page info div and set textContent to sanitize // Create page info div and set textContent to sanitize
@@ -110,11 +111,13 @@ function attachMoveButtons() {
event.preventDefault(); event.preventDefault();
var parent = this.closest(".list-group-item"); var parent = this.closest(".list-group-item");
//Get name of removed file //Get name of removed file
var fileName = parent.querySelector(".filename").innerText; let filenameNode = parent.querySelector(".filename");
var fileName = filenameNode.innerText;
const fileId = filenameNode.getAttribute("data-file-id");
parent.remove(); parent.remove();
updateFiles(); updateFiles();
//Dispatch a custom event with the name of the removed file //Dispatch a custom event with the name of the removed file
var event = new CustomEvent("fileRemoved", { detail: fileName }); var event = new CustomEvent("fileRemoved", { detail: fileId });
document.dispatchEvent(event); document.dispatchEvent(event);
}); });
} }

View File

@@ -1,12 +1,20 @@
import { DeletePageCommand } from "./commands/delete-page.js";
import { SelectPageCommand } from "./commands/select.js";
import { SplitFileCommand } from "./commands/split.js";
import { UndoManager } from "./UndoManager.js";
class PdfActionsManager { class PdfActionsManager {
pageDirection; pageDirection;
pagesContainer; pagesContainer;
static selectedPages = []; // Static property shared across all instances static selectedPages = []; // Static property shared across all instances
undoManager;
constructor(id) { constructor(id, undoManager) {
this.pagesContainer = document.getElementById(id); this.pagesContainer = document.getElementById(id);
this.pageDirection = document.documentElement.getAttribute("dir"); this.pageDirection = document.documentElement.getAttribute("dir");
this.undoManager = undoManager || new UndoManager();
var styleElement = document.createElement("link"); var styleElement = document.createElement("link");
styleElement.rel = "stylesheet"; styleElement.rel = "stylesheet";
styleElement.href = "css/pdfActions.css"; styleElement.href = "css/pdfActions.css";
@@ -27,7 +35,8 @@ class PdfActionsManager {
const sibling = imgContainer.previousSibling; const sibling = imgContainer.previousSibling;
if (sibling) { if (sibling) {
this.movePageTo(imgContainer, sibling, true); let movePageCommand = this.movePageTo(imgContainer, sibling, true, true);
this._pushUndoClearRedo(movePageCommand);
} }
} }
@@ -35,7 +44,12 @@ class PdfActionsManager {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
const sibling = imgContainer.nextSibling; const sibling = imgContainer.nextSibling;
if (sibling) { if (sibling) {
this.movePageTo(imgContainer, sibling.nextSibling, true); let movePageCommand = this.movePageTo(
imgContainer,
sibling.nextSibling,
true
);
this._pushUndoClearRedo(movePageCommand);
} }
} }
@@ -43,30 +57,27 @@ class PdfActionsManager {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
const img = imgContainer.querySelector("img"); const img = imgContainer.querySelector("img");
this.rotateElement(img, -90); let rotateCommand = this.rotateElement(img, -90);
this._pushUndoClearRedo(rotateCommand);
} }
rotateCWButtonCallback(e) { rotateCWButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
const img = imgContainer.querySelector("img"); const img = imgContainer.querySelector("img");
this.rotateElement(img, 90); let rotateCommand = this.rotateElement(img, 90);
this._pushUndoClearRedo(rotateCommand);
} }
deletePageButtonCallback(e) { deletePageButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target); let imgContainer = this.getPageContainer(e.target);
this.pagesContainer.removeChild(imgContainer); let deletePageCommand = new DeletePageCommand(
if (this.pagesContainer.childElementCount === 0) { imgContainer,
const filenameInput = document.getElementById("filename-input"); this.pagesContainer
const filenameParagraph = document.getElementById("filename"); );
const downloadBtn = document.getElementById("export-button"); deletePageCommand.execute();
filenameInput.disabled = true; this._pushUndoClearRedo(deletePageCommand);
filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
} }
insertFileButtonCallback(e) { insertFileButtonCallback(e) {
@@ -81,7 +92,15 @@ class PdfActionsManager {
splitFileButtonCallback(e) { splitFileButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target); var imgContainer = this.getPageContainer(e.target);
imgContainer.classList.toggle("split-before");
let splitFileCommand = new SplitFileCommand(imgContainer, "split-before");
splitFileCommand.execute();
this._pushUndoClearRedo(splitFileCommand);
}
_pushUndoClearRedo(command) {
this.undoManager.pushUndoClearRedo(command);
} }
setActions({ movePageTo, addFiles, rotateElement }) { setActions({ movePageTo, addFiles, rotateElement }) {
@@ -159,25 +178,10 @@ class PdfActionsManager {
selectCheckbox.onchange = () => { selectCheckbox.onchange = () => {
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1; const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
if (selectCheckbox.checked) { let selectPageCommand = new SelectPageCommand(pageNumber, selectCheckbox);
//adds to array of selected pages selectPageCommand.execute();
window.selectedPages.push(pageNumber);
} else {
//remove page from selected pages array
const index = window.selectedPages.indexOf(pageNumber);
if (index !== -1) {
window.selectedPages.splice(index, 1);
}
}
if (window.selectedPages.length > 0 && !window.selectPage) { this._pushUndoClearRedo(selectPageCommand);
window.toggleSelectPageVisibility();
}
if (window.selectedPages.length == 0 && window.selectPage) {
window.toggleSelectPageVisibility();
}
window.updateSelectedPagesDisplay();
}; };
const insertFileButtonContainer = document.createElement("div"); const insertFileButtonContainer = document.createElement("div");

View File

@@ -1,11 +1,18 @@
import { MovePageUpCommand, MovePageDownCommand } from "./commands/move-page.js";
import { RemoveSelectedCommand } from "./commands/remove.js";
import { RotateAllCommand, RotateElementCommand } from "./commands/rotate.js";
import { SplitAllCommand } from "./commands/split.js";
import { UndoManager } from "./UndoManager.js";
class PdfContainer { class PdfContainer {
fileName; fileName;
pagesContainer; pagesContainer;
pagesContainerWrapper; pagesContainerWrapper;
pdfAdapters; pdfAdapters;
downloadLink; downloadLink;
undoManager;
constructor(id, wrapperId, pdfAdapters) { constructor(id, wrapperId, pdfAdapters, undoManager) {
this.pagesContainer = document.getElementById(id); this.pagesContainer = document.getElementById(id);
this.pagesContainerWrapper = document.getElementById(wrapperId); this.pagesContainerWrapper = document.getElementById(wrapperId);
this.downloadLink = null; this.downloadLink = null;
@@ -31,6 +38,8 @@ class PdfContainer {
this.removeAllElements = this.removeAllElements.bind(this); this.removeAllElements = this.removeAllElements.bind(this);
this.resetPages = this.resetPages.bind(this); this.resetPages = this.resetPages.bind(this);
this.undoManager = undoManager || new UndoManager();
this.pdfAdapters = pdfAdapters; this.pdfAdapters = pdfAdapters;
this.pdfAdapters.forEach((adapter) => { this.pdfAdapters.forEach((adapter) => {
@@ -58,6 +67,33 @@ class PdfContainer {
window.removeAllElements = this.removeAllElements; window.removeAllElements = this.removeAllElements;
window.resetPages = this.resetPages; window.resetPages = this.resetPages;
let undoBtn = document.getElementById('undo-btn');
let redoBtn = document.getElementById('redo-btn');
document.addEventListener('undo-manager-update', (e) => {
let canUndo = e.detail.canUndo;
let canRedo = e.detail.canRedo;
undoBtn.disabled = !canUndo;
redoBtn.disabled = !canRedo;
})
window.undo = () => {
if (undoManager.canUndo()) undoManager.undo();
else {
undoBtn.disabled = !undoManager.canUndo();
redoBtn.disabled = !undoManager.canRedo();
}
}
window.redo = () => {
if (undoManager.canRedo()) undoManager.redo();
else {
undoBtn.disabled = !undoManager.canUndo();
redoBtn.disabled = !undoManager.canRedo();
}
}
const filenameInput = document.getElementById("filename-input"); const filenameInput = document.getElementById("filename-input");
const downloadBtn = document.getElementById("export-button"); const downloadBtn = document.getElementById("export-button");
@@ -68,32 +104,28 @@ class PdfContainer {
downloadBtn.disabled = true; downloadBtn.disabled = true;
} }
movePageTo(startElement, endElement, scrollTo = false) { movePageTo(startElement, endElement, scrollTo = false, moveUp = false) {
const childArray = Array.from(this.pagesContainer.childNodes); let movePageCommand;
const startIndex = childArray.indexOf(startElement); if (moveUp) {
const endIndex = childArray.indexOf(endElement); movePageCommand = new MovePageUpCommand(
startElement,
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move. endElement,
const pageNumberElement = startElement.querySelector(".page-number"); this.pagesContainer,
if (pageNumberElement) { this.pagesContainerWrapper,
startElement.removeChild(pageNumberElement); scrollTo
} );
this.pagesContainer.removeChild(startElement);
if (!endElement) {
this.pagesContainer.append(startElement);
} else { } else {
this.pagesContainer.insertBefore(startElement, endElement); movePageCommand = new MovePageDownCommand(
startElement,
endElement,
this.pagesContainer,
this.pagesContainerWrapper,
scrollTo
);
} }
if (scrollTo) { movePageCommand.execute();
const { width } = startElement.getBoundingClientRect(); return movePageCommand;
const vector = endIndex !== -1 && startIndex > endIndex ? 0 - width : width;
this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft + vector,
});
}
} }
addFiles(nextSiblingElement, blank = false) { addFiles(nextSiblingElement, blank = false) {
@@ -165,56 +197,22 @@ class PdfContainer {
async addFilesBlank(nextSiblingElement) { async addFilesBlank(nextSiblingElement) {
const pdfContent = ` let doc = await PDFLib.PDFDocument.create();
%PDF-1.4 let docBytes = await doc.save();
1 0 obj
<< /Type /Catalog /Pages 2 0 R >> const url = URL.createObjectURL(new Blob([docBytes], { type: 'application/pdf' }));
endobj
2 0 obj const renderer = await this.toRenderer(url);
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj await this.addPdfFile(renderer, doc, nextSiblingElement);
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Contents 5 0 R >>
endobj
5 0 obj
<< /Length 44 >>
stream
0 0 0 595 0 842 re
W
n
endstream
endobj
xref
0 6
0000000000 65535 f
0000000010 00000 n
0000000071 00000 n
0000000121 00000 n
0000000205 00000 n
0000000400 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
278
%%EOF
`;
const blob = new Blob([pdfContent], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const file = new File([blob], "blank_page.pdf", { type: "application/pdf" });
await this.addPdfFile(file, nextSiblingElement);
} }
rotateElement(element, deg) { rotateElement(element, deg) {
var lastTransform = element.style.rotate; let rotateCommand = new RotateElementCommand(element, deg);
if (!lastTransform) { rotateCommand.execute();
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const newAngle = lastAngle + deg;
element.style.rotate = newAngle + "deg";
return rotateCommand;
} }
async addPdfFile(renderer, pdfDocument, nextSiblingElement) { async addPdfFile(renderer, pdfDocument, nextSiblingElement) {
@@ -309,6 +307,7 @@ class PdfContainer {
} }
rotateAll(deg) { rotateAll(deg) {
let elementsToRotate = [];
for (let i = 0; i < this.pagesContainer.childNodes.length; i++) { for (let i = 0; i < this.pagesContainer.childNodes.length; i++) {
const child = this.pagesContainer.children[i]; const child = this.pagesContainer.children[i];
if (!child) continue; if (!child) continue;
@@ -320,8 +319,13 @@ class PdfContainer {
const img = child.querySelector("img"); const img = child.querySelector("img");
if (!img) continue; if (!img) continue;
this.rotateElement(img, deg); elementsToRotate.push(img);
} }
let rotateAllCommand = new RotateAllCommand(elementsToRotate, deg);
rotateAllCommand.execute();
this.undoManager.pushUndoClearRedo(rotateAllCommand);
} }
removeAllElements(){ removeAllElements(){
@@ -336,34 +340,13 @@ class PdfContainer {
deleteSelected() { deleteSelected() {
window.selectedPages.sort((a, b) => a - b); window.selectedPages.sort((a, b) => a - b);
let deletions = 0; let removeSelectedCommand = new RemoveSelectedCommand(
this.pagesContainer,
window.selectedPages,
this.updatePageNumbersAndCheckboxes
);
window.selectedPages.forEach((pageIndex) => { this.undoManager.pushUndoClearRedo(removeSelectedCommand);
const adjustedIndex = pageIndex - 1 - deletions;
const child = this.pagesContainer.children[adjustedIndex];
if (child) {
this.pagesContainer.removeChild(child);
deletions++;
}
});
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
if (filenameInput)
filenameInput.disabled = true;
filenameInput.value = "";
if (filenameParagraph)
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
window.selectedPages = [];
this.updatePageNumbersAndCheckboxes();
document.dispatchEvent(new Event("selectedPagesUpdated"));
} }
toggleSelectAll() { toggleSelectAll() {
@@ -531,34 +514,17 @@ class PdfContainer {
splitAll() { splitAll() {
const allPages = this.pagesContainer.querySelectorAll(".page-container"); const allPages = this.pagesContainer.querySelectorAll(".page-container");
let splitAllCommand = new SplitAllCommand(
allPages,
window.selectPage,
window.selectedPages,
"split-before"
);
splitAllCommand.execute();
if (!window.selectPage) { this.undoManager.pushUndoClearRedo(splitAllCommand);
const hasSplit = this.pagesContainer.querySelectorAll(".split-before").length > 0;
if (hasSplit) {
allPages.forEach(page => {
page.classList.remove("split-before");
});
} else {
allPages.forEach(page => {
page.classList.add("split-before");
});
}
return;
}
allPages.forEach((page, index) => {
const pageIndex = index;
if (window.selectPage && !window.selectedPages.includes(pageIndex)) return;
if (page.classList.contains("split-before")) {
page.classList.remove("split-before");
} else {
page.classList.add("split-before");
}
});
} }
async splitPDF(baseDocBytes, splitters) { async splitPDF(baseDocBytes, splitters) {
const baseDocument = await PDFLib.PDFDocument.load(baseDocBytes); const baseDocument = await PDFLib.PDFDocument.load(baseDocBytes);
const pageNum = baseDocument.getPages().length; const pageNum = baseDocument.getPages().length;

View File

@@ -0,0 +1,65 @@
export class UndoManager {
_undoStack;
_redoStack;
constructor() {
this._undoStack = [];
this._redoStack = [];
}
pushUndo(command) {
this._undoStack.push(command);
this._dispatchStateChange();
}
pushRedo(command) {
this._redoStack.push(command);
this._dispatchStateChange();
}
pushUndoClearRedo(command) {
this._undoStack.push(command);
this._redoStack = [];
this._dispatchStateChange();
}
undo() {
if (!this.canUndo()) return;
let cmd = this._undoStack.pop();
cmd.undo();
this._redoStack.push(cmd);
this._dispatchStateChange();
}
canUndo() {
return this._undoStack && this._undoStack.length > 0;
}
redo() {
if (!this.canRedo()) return;
let cmd = this._redoStack.pop();
cmd.redo();
this._undoStack.push(cmd);
this._dispatchStateChange();
}
canRedo() {
return this._redoStack && this._redoStack.length > 0;
}
_dispatchStateChange() {
document.dispatchEvent(
new CustomEvent("undo-manager-update", {
bubbles: true,
detail: {
canUndo: this.canUndo(),
canRedo: this.canRedo(),
},
})
);
}
}

View File

@@ -0,0 +1,5 @@
export class Command {
execute() {}
undo() {}
redo() {}
}

View File

@@ -0,0 +1,74 @@
import { Command } from "./command.js";
export class DeletePageCommand extends Command {
constructor(element, pagesContainer) {
super();
this.element = element;
this.pagesContainer = pagesContainer;
this.filenameInputValue = document.getElementById("filename-input").value;
const filenameParagraph = document.getElementById("filename");
this.filenameParagraphText = filenameParagraph ? filenameParagraph.innerText : "";
}
execute() {
this.nextSibling = this.element.nextSibling;
this.pagesContainer.removeChild(this.element);
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = true;
filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
}
undo() {
let node = this.nextSibling;
if (node) this.pagesContainer.insertBefore(this.element, node);
else this.pagesContainer.appendChild(this.element);
const pageNumberElement = this.element.querySelector(".page-number");
if (pageNumberElement) {
this.element.removeChild(pageNumberElement);
}
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = false;
filenameInput.value = this.filenameInputValue;
if (this.filenameParagraph)
filenameParagraph.innerText = this.filenameParagraphText;
downloadBtn.disabled = false;
}
redo() {
const pageNumberElement = this.element.querySelector(".page-number");
if (pageNumberElement) {
this.element.removeChild(pageNumberElement);
}
this.pagesContainer.removeChild(this.element);
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
filenameInput.disabled = true;
filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
}
}

View File

@@ -0,0 +1,133 @@
import { Command } from "./command.js";
export class AbstractMovePageCommand extends Command {
constructor(
startElement,
endElement,
pagesContainer,
pagesContainerWrapper,
scrollTo = false
) {
super();
this.pagesContainer = pagesContainer;
const childArray = Array.from(this.pagesContainer.childNodes);
this.startIndex = childArray.indexOf(startElement);
this.endIndex = childArray.indexOf(endElement);
this.startElement = startElement;
this.endElement = endElement;
this.scrollTo = scrollTo;
this.pagesContainerWrapper = pagesContainerWrapper;
}
execute() {
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move.
const pageNumberElement = this.startElement.querySelector(".page-number");
if (pageNumberElement) {
this.startElement.removeChild(pageNumberElement);
}
this.pagesContainer.removeChild(this.startElement);
if (!this.endElement) {
this.pagesContainer.append(this.startElement);
} else {
this.pagesContainer.insertBefore(this.startElement, this.endElement);
}
if (this.scrollTo) {
const { width } = this.startElement.getBoundingClientRect();
const vector =
this.endIndex !== -1 && this.startIndex > this.endIndex
? 0 - width
: width;
this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft + vector,
});
}
}
undo() {
// Requires overriding in child classes
}
redo() {
this.execute();
}
}
export class MovePageUpCommand extends AbstractMovePageCommand {
constructor(
startElement,
endElement,
pagesContainer,
pagesContainerWrapper,
scrollTo = false
) {
super(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo);
}
undo() {
if (this.endElement) {
this.pagesContainer.removeChild(this.endElement);
this.startElement.insertAdjacentElement("beforebegin", this.endElement);
}
if (this.scrollTo) {
const { width } = this.startElement.getBoundingClientRect();
const vector =
this.endIndex === -1 || this.startIndex <= this.endIndex
? 0 - width
: width;
this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft - vector,
});
}
}
redo() {
this.execute();
}
}
export class MovePageDownCommand extends AbstractMovePageCommand {
constructor(
startElement,
endElement,
pagesContainer,
pagesContainerWrapper,
scrollTo = false
) {
super(startElement, endElement, pagesContainer, pagesContainerWrapper, scrollTo);
}
undo() {
let previousElement = this.startElement.previousSibling;
if (this.startElement) {
this.pagesContainer.removeChild(this.startElement);
previousElement.insertAdjacentElement("beforebegin", this.startElement);
}
if (this.scrollTo) {
const { width } = this.startElement.getBoundingClientRect();
const vector =
this.endIndex === -1 || this.startIndex <= this.endIndex
? 0 - width
: width;
this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft - vector,
});
}
}
redo() {
this.execute();
}
}

View File

@@ -0,0 +1,101 @@
import { Command } from "./command.js";
export class RemoveSelectedCommand extends Command {
constructor(pagesContainer, selectedPages, updatePageNumbersAndCheckboxes) {
super();
this.pagesContainer = pagesContainer;
this.selectedPages = selectedPages;
this.deletedChildren = [];
if (updatePageNumbersAndCheckboxes) {
this.updatePageNumbersAndCheckboxes = updatePageNumbersAndCheckboxes;
} else {
const pageDivs = document.querySelectorAll(".pdf-actions_container");
pageDivs.forEach((div, index) => {
const pageNumber = index + 1;
const checkbox = div.querySelector(".pdf-actions_checkbox");
checkbox.id = `selectPageCheckbox-${pageNumber}`;
checkbox.setAttribute("data-page-number", pageNumber);
checkbox.checked = window.selectedPages.includes(pageNumber);
});
}
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
this.originalFilenameInputValue = filenameInput ? filenameInput.value : "";
if (filenameParagraph)
this.originalFilenameParagraphText = filenameParagraph.innerText;
}
execute() {
let deletions = 0;
this.selectedPages.forEach((pageIndex) => {
const adjustedIndex = pageIndex - 1 - deletions;
const child = this.pagesContainer.children[adjustedIndex];
if (child) {
this.pagesContainer.removeChild(child);
deletions++;
this.deletedChildren.push({
idx: adjustedIndex,
childNode: child,
});
}
});
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
if (filenameInput) filenameInput.disabled = true;
filenameInput.value = "";
if (filenameParagraph) filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
window.selectedPages = [];
this.updatePageNumbersAndCheckboxes();
document.dispatchEvent(new Event("selectedPagesUpdated"));
}
undo() {
while (this.deletedChildren.length > 0) {
let deletedChild = this.deletedChildren.pop();
if (this.pagesContainer.children.length <= deletedChild.idx)
this.pagesContainer.appendChild(deletedChild.childNode);
else {
this.pagesContainer.insertBefore(
deletedChild.childNode,
this.pagesContainer.children[deletedChild.idx]
);
}
}
if (this.pagesContainer.childElementCount > 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
if (filenameInput) filenameInput.disabled = false;
filenameInput.value = this.originalFilenameInputValue;
if (filenameParagraph)
filenameParagraph.innerText = this.originalFilenameParagraphText;
downloadBtn.disabled = false;
}
window.selectedPages = this.selectedPages;
this.updatePageNumbersAndCheckboxes();
document.dispatchEvent(new Event("selectedPagesUpdated"));
}
redo() {
this.execute();
}
}

View File

@@ -0,0 +1,74 @@
import { Command } from "./command.js";
export class RotateElementCommand extends Command {
constructor(element, degree) {
super();
this.element = element;
this.degree = degree;
}
execute() {
let lastTransform = this.element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const newAngle = lastAngle + parseInt(this.degree);
this.element.style.rotate = newAngle + "deg";
}
undo() {
let lastTransform = this.element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const undoAngle = currentAngle + -parseInt(this.degree);
this.element.style.rotate = undoAngle + "deg";
}
redo() {
this.execute();
}
}
export class RotateAllCommand extends Command {
constructor(elements, degree) {
super();
this.elements = elements;
this.degree = degree;
}
execute() {
for (let element of this.elements) {
let lastTransform = element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const newAngle = lastAngle + this.degree;
element.style.rotate = newAngle + "deg";
}
}
undo() {
for (let element of this.elements) {
let lastTransform = element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const currentAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const undoAngle = currentAngle + -this.degree;
element.style.rotate = undoAngle + "deg";
}
}
redo() {
this.execute();
}
}

View File

@@ -0,0 +1,59 @@
import { Command } from "./command.js";
export class SelectPageCommand extends Command {
constructor(pageNumber, checkbox) {
super();
this.pageNumber = pageNumber;
this.selectCheckbox = checkbox;
}
execute() {
if (this.selectCheckbox.checked) {
//adds to array of selected pages
window.selectedPages.push(this.pageNumber);
} else {
//remove page from selected pages array
const index = window.selectedPages.indexOf(this.pageNumber);
if (index !== -1) {
window.selectedPages.splice(index, 1);
}
}
if (window.selectedPages.length > 0 && !window.selectPage) {
window.toggleSelectPageVisibility();
}
if (window.selectedPages.length == 0 && window.selectPage) {
window.toggleSelectPageVisibility();
}
window.updateSelectedPagesDisplay();
}
undo() {
this.selectCheckbox.checked = !this.selectCheckbox.checked;
if (this.selectCheckbox.checked) {
//adds to array of selected pages
window.selectedPages.push(this.pageNumber);
} else {
//remove page from selected pages array
const index = window.selectedPages.indexOf(this.pageNumber);
if (index !== -1) {
window.selectedPages.splice(index, 1);
}
}
if (window.selectedPages.length > 0 && !window.selectPage) {
window.toggleSelectPageVisibility();
}
if (window.selectedPages.length == 0 && window.selectPage) {
window.toggleSelectPageVisibility();
}
window.updateSelectedPagesDisplay();
}
redo() {
this.selectCheckbox.checked = !this.selectCheckbox.checked;
this.execute();
}
}

View File

@@ -0,0 +1,101 @@
import { Command } from "./command.js";
export class SplitFileCommand extends Command {
constructor(element, splitClass) {
super();
this.element = element;
this.splitClass = splitClass;
}
execute() {
this.element.classList.toggle(this.splitClass);
}
undo() {
this.element.classList.toggle(this.splitClass);
}
redo() {
this.execute();
}
}
export class SplitAllCommand extends Command {
constructor(elements, isSelectedInWindow, selectedPages, splitClass) {
super();
this.elements = elements;
this.isSelectedInWindow = isSelectedInWindow;
this.selectedPages = selectedPages;
this.splitClass = splitClass;
}
execute() {
if (!this.isSelectedInWindow) {
const hasSplit = this._hasSplit(this.elements, this.splitClass);
if (hasSplit) {
this.elements.forEach((page) => {
page.classList.remove(this.splitClass);
});
} else {
this.elements.forEach((page) => {
page.classList.add(this.splitClass);
});
}
return;
}
this.elements.forEach((page, index) => {
const pageIndex = index;
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
return;
if (page.classList.contains(this.splitClass)) {
page.classList.remove(this.splitClass);
} else {
page.classList.add(this.splitClass);
}
});
}
_hasSplit() {
if (!this.elements || this.elements.length == 0) return false;
for (const node of this.elements) {
if (node.classList.contains(this.splitClass)) return true;
}
return false;
}
undo() {
if (!this.isSelectedInWindow) {
const hasSplit = this._hasSplit(this.elements, this.splitClass);
if (hasSplit) {
this.elements.forEach((page) => {
page.classList.remove(this.splitClass);
});
} else {
this.elements.forEach((page) => {
page.classList.add(this.splitClass);
});
}
return;
}
this.elements.forEach((page, index) => {
const pageIndex = index;
if (this.isSelectedInWindow && !this.selectedPages.includes(pageIndex))
return;
if (page.classList.contains(this.splitClass)) {
page.classList.remove(this.splitClass);
} else {
page.classList.add(this.splitClass);
}
});
}
redo() {
this.execute();
}
}

View File

@@ -0,0 +1,9 @@
class UUID {
static uuidv4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
);
}
}
export default UUID;

View File

@@ -189,7 +189,7 @@
<label for="authType">Authentication Type</label> <label for="authType">Authentication Type</label>
<select id="authType" name="authType" class="form-control" required> <select id="authType" name="authType" class="form-control" required>
<option value="web" selected>WEB</option> <option value="web" selected>WEB</option>
<option value="oauth2">OAUTH2</option> <option value="sso">SSO</option>
</select> </select>
</div> </div>
<div class="form-check mb-3" id="checkboxContainer"> <div class="form-check mb-3" id="checkboxContainer">
@@ -267,7 +267,7 @@
var passwordFieldContainer = $('#passwordContainer'); var passwordFieldContainer = $('#passwordContainer');
var checkboxContainer = $('#checkboxContainer'); var checkboxContainer = $('#checkboxContainer');
if (authType === 'oauth2') { if (authType === 'sso') {
passwordField.removeAttr('required'); passwordField.removeAttr('required');
passwordField.prop('disabled', true).val(''); passwordField.prop('disabled', true).val('');
passwordFieldContainer.slideUp('fast'); passwordFieldContainer.slideUp('fast');

View File

@@ -22,7 +22,7 @@
<input id="y" type="hidden" name="y"> <input id="y" type="hidden" name="y">
<input id="width" type="hidden" name="width"> <input id="width" type="hidden" name="width">
<input id="height" type="hidden" name="height"> <input id="height" type="hidden" name="height">
<button type="submit" class="btn btn-primary" th:text="#{crop.submit}"></button> <button id="submitBtn" type="submit" class="btn btn-primary" th:text="#{crop.submit}"></button>
</form> </form>
<div id="canvasesContainer" style="position: relative; margin: 20px 0; width: auto;"> <div id="canvasesContainer" style="position: relative; margin: 20px 0; width: auto;">
<canvas id="cropPdfCanvas" style="width: 100%"></canvas> <canvas id="cropPdfCanvas" style="width: 100%"></canvas>

View File

@@ -59,7 +59,7 @@
<link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}"> <link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}">
<link rel="stylesheet" th:href="@{'/css/footer.css'}"> <link rel="stylesheet" th:href="@{'/css/footer.css'}">
<link rel="preload" href="/fonts/google-symbol.woff2" as="font" type="font/woff2" crossorigin="anonymous"> <link rel="preload" th:href="@{'/fonts/google-symbol.woff2'}" as="font" type="font/woff2" crossorigin="anonymous">
<script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script> <script th:src="@{'/js/thirdParty/fontfaceobserver.standalone.js'}"></script>
@@ -204,11 +204,17 @@
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script> <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script th:src="@{'/js/downloader.js'}"></script> <script th:src="@{'/js/downloader.js'}"></script>
<div class="custom-file-chooser" th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}"> <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"> <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}">
<input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" th:attr="multiple=${!disableMultipleFiles}" th:required="${notRequired} ? null : 'required'"> <label class="file-input-btn d-none">
<input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept}" th:attr="multiple=${!disableMultipleFiles}" th:required="${notRequired} ? null : 'required'">
Browse
</label>
<div th:text="#{fileChooser.click}"></div>
<div th:text="#{fileChooser.or}"></div>
<div th:text="#{fileChooser.dragAndDrop}"></div>
</div> </div>
<div class="selected-files"></div> <div class="selected-files flex-wrap"></div>
</div> </div>
<div class="progressBarContainer" style="display: none; position: relative;"> <div class="progressBarContainer" style="display: none; position: relative;">
@@ -218,5 +224,5 @@
</div> </div>
</div> </div>
</div> </div>
<script th:src="@{'/js/fileInput.js'}"></script> <script th:src="@{'/js/fileInput.js'}" type="module"></script>
</th:block> </th:block>

View File

@@ -164,13 +164,13 @@
keyInput.type = "text"; keyInput.type = "text";
keyInput.placeholder = 'Key'; keyInput.placeholder = 'Key';
keyInput.className = "form-control"; keyInput.className = "form-control";
keyInput.name = "customKey" + count; keyInput.name = `allRequestParams[customKey${count}]`;
const valueInput = document.createElement("input"); const valueInput = document.createElement("input");
valueInput.type = "text"; valueInput.type = "text";
valueInput.placeholder = 'Value'; valueInput.placeholder = 'Value';
valueInput.className = "form-control"; valueInput.className = "form-control";
valueInput.name = "customValue" + count; valueInput.name = `allRequestParams[customValue${count}]`;
count = count + 1; count = count + 1;
const formGroup = document.createElement("div"); const formGroup = document.createElement("div");

View File

@@ -156,7 +156,7 @@
resultDiv2.innerHTML = loading; resultDiv2.innerHTML = loading;
// Create a new Worker // Create a new Worker
const worker = new Worker('/js/compare/pdfWorker.js'); const worker = new Worker('./js/compare/pdfWorker.js');
// Post messages to the worker // Post messages to the worker

View File

@@ -57,6 +57,27 @@
</span> </span>
<span class="btn-tooltip" th:text="#{multiTool.insertPageBreak}"></span> <span class="btn-tooltip" th:text="#{multiTool.insertPageBreak}"></span>
</button> </button>
<button id="undo-btn" class="btn btn-secondary" onclick="undo()" disabled>
<span class="material-symbols-rounded">
undo
</span>
<span class="btn-tooltip">
<div th:text="#{multiTool.undo}"></div>
<div class="text-uppercase" th:text="'(CTRL + Z)'"></div>
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" onclick="redo()" disabled>
<span class="material-symbols-rounded">
redo
</span>
<span class="btn-tooltip">
<div th:text="#{multiTool.redo}"></div>
<div class="text-uppercase" th:text="'(CTRL + Y)'"></div>
</span>
</button>
</button>
<button id="select-pages-container" class="btn btn-secondary enable-on-file" <button id="select-pages-container" class="btn btn-secondary enable-on-file"
onclick="toggleSelectPageVisibility()" disabled> onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded"> <span id="select-pages-button" class="material-symbols-rounded">
@@ -139,7 +160,9 @@
split: '[[#{multiTool.split}]]', split: '[[#{multiTool.split}]]',
addFile: '[[#{multiTool.addFile}]]', addFile: '[[#{multiTool.addFile}]]',
insertPageBreak:'[[#{multiTool.insertPageBreak}]]', insertPageBreak:'[[#{multiTool.insertPageBreak}]]',
dragDropMessage:'[[#{multiTool.dragDropMessage}]]' dragDropMessage:'[[#{multiTool.dragDropMessage}]]',
undo: '[[#{multiTool.undo}]]',
redo: '[[#{multiTool.redo}]]'
}; };
@@ -155,17 +178,20 @@
}); });
</script> </script>
<script type="module"> <script type="module">
import { UndoManager } from './js/multitool/UndoManager.js';
import PdfContainer from './js/multitool/PdfContainer.js'; import PdfContainer from './js/multitool/PdfContainer.js';
import DragDropManager from "./js/multitool/DragDropManager.js"; import DragDropManager from "./js/multitool/DragDropManager.js";
import ImageHighlighter from "./js/multitool/ImageHighlighter.js"; import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
import PdfActionsManager from './js/multitool/PdfActionsManager.js'; import PdfActionsManager from './js/multitool/PdfActionsManager.js';
import FileDragManager from './js/multitool/fileInput.js'; import FileDragManager from './js/multitool/fileInput.js';
// enables drag and drop // enables drag and drop
var undoManager = new UndoManager();
const dragDropManager = new DragDropManager('drag-container', 'pages-container'); const dragDropManager = new DragDropManager('drag-container', 'pages-container');
// enables image highlight on click // enables image highlight on click
const imageHighlighter = new ImageHighlighter('image-highlighter'); const imageHighlighter = new ImageHighlighter('image-highlighter');
// enables the default action buttons on each file // enables the default action buttons on each file
const pdfActionsManager = new PdfActionsManager('pages-container'); const pdfActionsManager = new PdfActionsManager('pages-container', undoManager);
const fileDragManager = new FileDragManager(); const fileDragManager = new FileDragManager();
// Scroll the wrapper horizontally // Scroll the wrapper horizontally
@@ -178,10 +204,23 @@
imageHighlighter, imageHighlighter,
pdfActionsManager, pdfActionsManager,
fileDragManager fileDragManager
] ],
undoManager
) )
fileDragManager.setCallback(async (files) => pdfContainer.addFilesFromFiles(files)); fileDragManager.setCallback(async (files) => pdfContainer.addFilesFromFiles(files));
document.addEventListener('keydown', function(event) {
let targetElementId = event.target.id;
// To avoid undoing/redoing the page when the user is simply undoing/redoing text
const isFilenameInputField = (targetElementId === 'filename-input') && (event.target === document.activeElement);
const isUndo = (event.ctrlKey && event.key === 'z');
const isRedo = (event.ctrlKey && event.key == 'y');
if (isUndo && !isFilenameInputField)
undoManager.undo();
else if (isRedo && !isFilenameInputField) undoManager.redo();
});
</script> </script>
</body> </body>

10
test.sh
View File

@@ -73,8 +73,8 @@ main() {
# Building Docker images # Building Docker images
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile . # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite . # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
# Test each configuration # Test each configuration
#run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml" #run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml"
@@ -93,9 +93,9 @@ main() {
# Building Docker images with security enabled # Building Docker images with security enabled
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile . # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite . # docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
# Test each configuration with security # Test each configuration with security