Compare commits

..

29 Commits

Author SHA1 Message Date
Anthony Stirling
fbc6b3a70e fix 2024-11-29 15:05:10 +00:00
Anthony Stirling
329f755823 Merge branch 'main' into saml 2024-11-29 14:32:04 +00:00
Anthony Stirling
acc50e48c5 debugs 2024-11-29 14:31:15 +00:00
Anthony Stirling
92a571d31c saml stuff 2024-11-29 14:29:54 +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
Anthony Stirling
5976e69f54 debugs 2024-11-29 10:40:10 +00:00
Anthony Stirling
e588d8f99e spring dev fix for saml 2024-11-29 09:06:51 +00:00
Anthony Stirling
1c0f423510 remove unused repo 2024-11-29 08:59:29 +00:00
Anthony Stirling
2d6fe55985 info to debug 2024-11-29 08:53:54 +00:00
Anthony Stirling
5171088fca more fixes 2024-11-29 08:43:57 +00:00
Anthony Stirling
b4837df76c ee flag for saml 2024-11-28 19:41:39 +00:00
Anthony Stirling
d20e8f7d54 oauth to saml and compare fixes etc 2024-11-28 19:27:37 +00:00
Anthony Stirling
2885fac30d remove debugs 2024-11-28 15:43:24 +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
Anthony Stirling
2a4a19a80f backup 2024-11-28 15:00:37 +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
78 changed files with 1781 additions and 604 deletions

View File

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

View File

@@ -21,6 +21,8 @@ ext {
imageioVersion = "3.12.0"
lombokVersion = "1.18.36"
bouncycastleVersion = "1.79"
springSecuritySamlVersion = "6.4.1"
openSamlVersion = "4.3.2"
}
group = "stirling.software"
@@ -144,17 +146,18 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$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'
// Don't upgrade h2database
runtimeOnly "com.h2database:h2:2.3.232"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
implementation "org.opensaml:opensaml-core:$openSamlVersion"
implementation "org.opensaml:opensaml-saml-api:$openSamlVersion"
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'
@@ -186,7 +189,7 @@ dependencies {
// Image metadata extractor
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"
//general PDF

View File

@@ -48,24 +48,6 @@ Feature: API Validation
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
Scenario: Process PDF with OCR
Given I generate a PDF file as "fileInput"
@@ -83,26 +65,6 @@ Feature: API Validation
Then the response content type should be "application/pdf"
And the response file should have size greater than 0
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

View File

@@ -3,13 +3,14 @@ package stirling.software.SPDF.EE;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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 stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Lazy
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class EEAppConfig {

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,20 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.*;
import org.opensaml.saml.saml2.core.AuthnRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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.saml2.core.Saml2X509Credential;
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.registration.InMemoryRelyingPartyRegistrationRepository;
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.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.authentication.UsernamePasswordAuthenticationFilter;
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.CsrfTokenRequestAttributeHandler;
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.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 stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
@@ -64,6 +84,7 @@ import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@EnableWebSecurity
@EnableMethodSecurity
@Slf4j
@DependsOn("runningEE")
public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService;
@@ -79,6 +100,10 @@ public class SecurityConfiguration {
@Qualifier("loginEnabled")
public boolean loginEnabledValue;
@Autowired
@Qualifier("runningEE")
public boolean runningEE;
@Autowired ApplicationProperties applicationProperties;
@Autowired private UserAuthenticationFilter userAuthenticationFilter;
@@ -90,13 +115,14 @@ public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
}
if (loginEnabledValue) {
http.addFilterBefore(
userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
@@ -137,7 +163,7 @@ public class SecurityConfiguration {
http.sessionManagement(
sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(10)
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry)
@@ -245,12 +271,23 @@ public class SecurityConfiguration {
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ()
&& applicationProperties.getSystem().getEnableAlphaFunctionality()) {
http.authenticationProvider(samlAuthenticationProvider());
http.saml2Login(
saml2 ->
if (applicationProperties.getSecurity().isSaml2Activ()) { // && runningEE
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
http.authenticationProvider(authenticationProvider)
.saml2Login(
saml2 -> {
try {
saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrations())
.authenticationManager(
new ProviderManager(authenticationProvider))
.successHandler(
new CustomSaml2AuthenticationSuccessHandler(
loginAttemptService,
@@ -258,14 +295,18 @@ public class SecurityConfiguration {
userService))
.failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.permitAll())
.addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
.authenticationRequestResolver(
authenticationRequestResolver(
relyingPartyRegistrations()));
} catch (Exception e) {
log.error("Error configuring SAML2 login", e);
throw new RuntimeException(e);
}
});
}
} else {
if (applicationProperties.getSecurity().getCsrfDisabled()) {
http.csrf(csrf -> csrf.disable());
} else {
if (!applicationProperties.getSecurity().getCsrfDisabled()) {
CookieCsrfTokenRepository cookieRepo =
CookieCsrfTokenRepository.withHttpOnlyFalse();
CsrfTokenRequestAttributeHandler requestHandler =
@@ -282,20 +323,6 @@ public class SecurityConfiguration {
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
@ConditionalOnProperty(
value = "security.oauth2.enabled",
@@ -425,18 +452,19 @@ public class SecurityConfiguration {
.clientName("OIDC")
.build());
}
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
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();
Saml2X509Credential signingCredential =
@@ -445,26 +473,97 @@ public class SecurityConfiguration {
CertificateUtils.readCertificate(certificateResource),
Saml2X509CredentialType.SIGNING);
X509Certificate idpCert = CertificateUtils.readCertificate(samlConf.getidpCert());
Saml2X509Credential verificationCredential = Saml2X509Credential.verification(idpCert);
RelyingPartyRegistration rp =
RelyingPartyRegistration.withRegistrationId(samlConf.getRegistrationId())
.signingX509Credentials((c) -> c.add(signingCredential))
.signingX509Credentials(c -> c.add(signingCredential))
.assertingPartyMetadata(
(details) ->
details.entityId(samlConf.getIdpIssuer())
metadata ->
metadata.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
(c) -> c.add(verificationCredential))
c -> c.add(verificationCredential))
.singleSignOnServiceBinding(
Saml2MessageBinding.POST)
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@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() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
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.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
@@ -50,8 +51,19 @@ public class UserService implements UserServiceInterface {
@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.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
public boolean processSSOPostLogin(String username, boolean autoCreateUser)
throws IllegalArgumentException, IOException {
if (!isUsernameValid(username)) {
return false;
@@ -61,7 +73,7 @@ public class UserService implements UserServiceInterface {
return true;
}
if (autoCreateUser) {
saveUser(username, AuthenticationType.OAUTH2);
saveUser(username, AuthenticationType.SSO);
return true;
}
return false;

View File

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

View File

@@ -3,12 +3,14 @@ package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
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.PemReader;
import org.springframework.core.io.Resource;
@@ -28,15 +30,26 @@ public class CertificateUtils {
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
try (PemReader pemReader =
new PemReader(
try (PEMParser pemParser =
new PEMParser(
new InputStreamReader(
privateKeyResource.getInputStream(), StandardCharsets.UTF_8))) {
PemObject pemObject = pemReader.readPemObject();
byte[] decodedKey = pemObject.getContent();
return (RSAPrivateKey)
KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
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.HttpSession;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
@@ -20,11 +21,11 @@ import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
@Slf4j
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@@ -34,10 +35,12 @@ public class CustomSaml2AuthenticationSuccessHandler
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
log.debug("Starting SAML2 authentication success handling");
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
log.debug("Authenticated principal found for user: {}", username);
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
@@ -45,46 +48,77 @@ public class CustomSaml2AuthenticationSuccessHandler
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
log.debug(
"Session exists: {}, Saved request exists: {}",
session != null,
savedRequest != null);
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
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);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
log.debug(
"Processing SAML2 authentication with autoCreateUser: {}",
saml2.getAutoCreateUser());
if (loginAttemptService.isBlocked(username)) {
log.debug("User {} is blocked due to too many login attempts", username);
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& saml2.getAutoCreateUser()) {
boolean userExists = userService.usernameExistsIgnoreCase(username);
boolean hasPassword = userExists && userService.hasPassword(username);
boolean isSSOUser =
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(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
if (saml2.getBlockRegistration() && !userExists) {
log.debug("Registration blocked for new user: {}", username);
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
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 + "/");
return;
} catch (IllegalArgumentException e) {
log.debug(
"Invalid username detected for user: {}, redirecting to logout",
username);
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
log.debug("Non-SAML2 principal detected, delegating to parent handler");
super.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security.saml2;
import java.util.*;
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.Attribute;
import org.opensaml.saml.saml2.core.AttributeStatement;
@@ -30,15 +28,60 @@ public class CustomSaml2ResponseAuthenticationConverter
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
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
// Debug log with actual values
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");
if (userOpt.isPresent()) {
User user = userOpt.get();
@@ -48,39 +91,27 @@ public class CustomSaml2ResponseAuthenticationConverter
}
}
// Extract the SessionIndexes
List<String> sessionIndexes = new ArrayList<>();
for (AuthnStatement authnStatement : assertion.getAuthnStatements()) {
sessionIndexes.add(authnStatement.getSessionIndex());
}
// Extract the Attributes
Map<String, List<Object>> attributes = extractAttributes(assertion);
// Create the custom principal
CustomSaml2AuthenticatedPrincipal principal =
new CustomSaml2AuthenticatedPrincipal(nameId, attributes, nameId, sessionIndexes);
new CustomSaml2AuthenticatedPrincipal(
userIdentifier, attributes, userIdentifier, sessionIndexes);
// Create the Saml2Authentication
return new Saml2Authentication(
principal,
responseToken.getToken().getSaml2Response(),
Collections.singletonList(simpleGrantedAuthority));
}
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()) {
log.info("BOOL: " + ((XSBoolean) xmlObject).getValue());
values.add(((XSString) xmlObject).getValue());
}
attributes.put(attributeName, values);
}
}
return attributes;
private boolean hasAttribute(Map<String, List<Object>> attributes, String name) {
return attributes.containsKey(name) && !attributes.get(name).isEmpty();
}
private String getFirstAttributeValue(Map<String, List<Object>> attributes, String name) {
List<Object> values = attributes.get(name);
return values != null && !values.isEmpty() ? values.get(0).toString() : null;
}
}

View File

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

View File

@@ -1,7 +1,5 @@
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.io.BufferedReader;
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.multipart.MultipartFile;
import io.github.pixee.security.BoundedLineReader;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
@@ -176,7 +176,9 @@ public class OCRController {
// Read the final PDF file
byte[] pdfContent = Files.readAllBytes(finalOutputFile);
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()).replaceFirst("[.][^.]+$", "") + "_OCR.pdf";
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_OCR.pdf";
return ResponseEntity.ok()
.header(

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package stirling.software.SPDF.repository;
import java.util.List;
import java.util.Optional;
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> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
}

View File

@@ -39,6 +39,13 @@ public class MetricsAggregatorService {
if (method == null || uri == null) {
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.format(

View File

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

View File

@@ -105,7 +105,7 @@ public class FileToPdf {
new ByteArrayInputStream(Files.readAllBytes(zipFilePath)))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempUnzippedDir.resolve(entry.getName());
Path filePath = tempUnzippedDir.resolve(sanitizeZipFilename(entry.getName()));
if (!entry.isDirectory()) {
Files.createDirectories(filePath.getParent());
if (entry.getName().toLowerCase().endsWith(".html")
@@ -175,7 +175,7 @@ public class FileToPdf {
ZipSecurity.createHardenedInputStream(new ByteArrayInputStream(fileBytes))) {
ZipEntry entry = zipIn.getNextEntry();
while (entry != null) {
Path filePath = tempDirectory.resolve(entry.getName());
Path filePath = tempDirectory.resolve(sanitizeZipFilename(entry.getName()));
if (entry.isDirectory()) {
Files.createDirectories(filePath); // Explicitly create the directory structure
} else {
@@ -241,4 +241,14 @@ public class FileToPdf {
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

@@ -3,6 +3,10 @@ multipart.enabled=true
logging.level.org.springframework=WARN
logging.level.org.hibernate=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
spring.jpa.open-in-view=false
@@ -27,6 +31,8 @@ server.servlet.context-path=${SYSTEM_ROOTURIPATH:/}
spring.devtools.restart.enabled=true
spring.devtools.livereload.enabled=true
spring.devtools.restart.exclude=stirling.software.SPDF.config.security/**
spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=تحريك إلى اليسار
multiTool.moveRight=تحريك إلى اليمين
multiTool.delete=حذف
multiTool.dragDropMessage=الصفحات المحددة
multiTool.undo=تراجع
multiTool.redo=إعادة إجراء
#multiTool-advert
multiTool-advert.message=هذه الميزة متوفرة في <a href="{0}">صفحة الأدوات المتعددة</a> لدينا. اطلع عليها للحصول على واجهة مستخدم محسّنة لكل صفحة وميزات إضافية!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=مستوى الإشارة المرجعية: اختر مس
splitByChapters.desc.3=تمثيل البيانات الأصلية: إذا تم اختيارها، سترمز البيانات المرجعية الأصلية إلى كل PDF مجزأ.
splitByChapters.desc.4=سماح بالتكرار: إذا تم اختياره، يسمح بوجود معاينات متعددة في الصفحة نفسها لخلق ملفات PDF منفصلة.
splitByChapters.submit=تقطيع ملف PDF
#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.large=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
username=İstifadəçi Adı
password=Şifrə
password=Şifr
welcome=Xoş gəldiniz
property=Xüsusiyyət
black=Qara
@@ -53,11 +53,11 @@ no=Xeyr
changedCredsMessage=Etibarnamələr dəyişdirildi!
notAuthenticatedMessage=İstifadəçinin kimliyi təsdiqlənməyib.
userNotFoundMessage=İstifadəçi tapılmadı.
incorrectPasswordMessage=Cari şifrə yanlışdır.
incorrectPasswordMessage=Cari şifr yanlışdır.
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.
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.
invalidPasswordMessage=Şifr boş olmamalıdır, başlanğıc və sonunda boşluqdan istifadə edilməməlidir.
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.
deleteUsernameExistsMessage=İstifadəçi adı mövcud deyildir və silinə bilməz.
downgradeCurrentUserMessage=Cari istifadəçinin rolunu aşağı salmaq mümkün deyil
@@ -70,8 +70,8 @@ oops=Oops!
help=Yardım
goHomepage=Ana səhifəyə get
joinDiscord=Discord serverimizə qatıl
seeDockerHub=Docker Hub'a bax
visitGithub=Github Repository'ə Baş Çək
seeDockerHub=Docker Hub-a bax
visitGithub=Github Repository-ə Baş Çək
donate=İanə Ver
color=Rəng
sponsor=Sponsor
@@ -126,9 +126,9 @@ enterpriseEdition.ssoAdvert=Daha çox istifadəçi-idarəetmə xüsusiyyətləri
#################
# 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.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.disable=Analitikanı deaktivləşdir
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.settings=Parametrlər
navbar.allTools=Alətlər
navbar.multiTool=Çox Alət
navbar.multiTool=Multi-Alət
navbar.search=Axtar
navbar.sections.organize=Təşkil et
navbar.sections.convertTo=PDF'ə Çevir
navbar.sections.convertFrom=PDF'dən Çevir
navbar.sections.convertTo=PDF-ə Çevir
navbar.sections.convertFrom=PDF-dən Çevir
navbar.sections.security=İmza & Təhlükəsizlik
navbar.sections.advance=Qabaqcıl
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.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.oldPassword=Cari Şifrə
changeCreds.newPassword=Yeni Şifrə
changeCreds.confirmNewPassword=Yeni Şifrəni Təsdiqləyin
changeCreds.oldPassword=Cari Şifr
changeCreds.newPassword=Yeni Şifr
changeCreds.confirmNewPassword=Yeni Şifri Təsdiqləyin
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.changeUsername=İstifadəçi Adını Dəyiş
account.newUsername=Yeni İstifadəçi Adı
account.password=Təsdiqləmə Şifrəsi
account.oldPassword=Keçmiş Şifrə
account.newPassword=Yeni Şifrə
account.changePassword=Şifrəni Dəyiş
account.confirmNewPassword=Yeni Şifrəni Təsdiqlə
account.password=Təsdiqləmə Şifri
account.oldPassword=Keçmiş Şifr
account.newPassword=Yeni Şifr
account.changePassword=Şifri Dəyiş
account.confirmNewPassword=Yeni Şifri Təsdiqlə
account.signOut=Çıxış
account.yourApiKey=Sizin API Açarınız
account.syncTitle=Brauzer parametrlərini hesabla sinxronlaşdırın
@@ -543,7 +543,7 @@ login.title=Daxil olun
login.header=Daxil olun
login.signin=Daxil olun
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.signinTitle=Zəhmət olmasa, daxil olun
login.ssoSignIn=Single Sign-on vasitəsilə daxil olun
@@ -581,8 +581,8 @@ showJS.submit=Göstər
#pdfToSinglePage
pdfToSinglePage.title=PDF'dən Tək Səhifəyə
pdfToSinglePage.header=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.submit=Tək Səhifəyə Çevir
@@ -601,8 +601,8 @@ getPdfInfo.downloadJson=JSON yüklə
#markdown-to-pdf
MarkdownToPDF.title=Markdown'dan PDF'ə
MarkdownToPDF.header=Markdown'dan PDF'ə
MarkdownToPDF.title=Markdown-dan PDF-ə
MarkdownToPDF.header=Markdown-dan PDF-ə
MarkdownToPDF.submit=Çevir
MarkdownToPDF.help=İş davam edir
MarkdownToPDF.credit=WeasyPrint İstifadə Edir
@@ -617,8 +617,8 @@ URLToPDF.credit=WeasyPrint İstifadə Edir
#html-to-pdf
HTMLToPDF.title=HTML'dən PDF'ə
HTMLToPDF.header=HTML'dən PDF'ə
HTMLToPDF.title=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.submit=Çevir
HTMLToPDF.credit=WeasyPrint İstifadə Edir
@@ -656,14 +656,14 @@ AddStampRequest.submit=Təsdiqlə
#sanitizePDF
sanitizePDF.title=PDF'i Təmizlə
sanitizePDF.title=PDF-i Təmizlə
sanitizePDF.header=PDF Faylını Təmizlə
sanitizePDF.selectText.1=JavaScript Fəaliyyətlərini Sil
sanitizePDF.selectText.2=Daxil Edilmiş Faylları Sil
sanitizePDF.selectText.3=Metadatanı Sil
sanitizePDF.selectText.4=Linkləri Sil
sanitizePDF.selectText.5=Şriftləri Sil
sanitizePDF.submit=PDF'i Təmizlə
sanitizePDF.submit=PDF-i Təmizlə
#addPageNumbers
@@ -683,7 +683,7 @@ addPageNumbers.submit=Səhifə Nömrələri əlavə edin
#auto-rename
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
@@ -698,7 +698,7 @@ adjustContrast.download=Yüklə
#crop
crop.title=Kəs
crop.header=Pdf'ləri Kəs
crop.header=Pdf-ləri Kəs
crop.submit=Təsdiq Et
@@ -722,8 +722,8 @@ pipeline.title=Pipeline
#pageLayout
pageLayout.title=Çoxsəhifəli Sxem
pageLayout.header=Çoxsəhifəli Sxem
pageLayout.title=Çoxsəhifəli Tərtibat
pageLayout.header=Çoxsəhifəli Tərtibat
pageLayout.pagesPerSheet=Vərəqdəki Səhifə Sayı:
pageLayout.addBorder=Çərçivə Əlavə Et
pageLayout.submit=Təsdiq et
@@ -739,22 +739,22 @@ scalePages.submit=Təsdiq edin
#certSign
certSign.title=Certificate Signing
certSign.header=Sign a PDF with your certificate (Work in progress)
certSign.selectPDF=Select a PDF File for Signing:
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.selectKey=Select Your Private Key File (PKCS#8 format, could be .pem or .der):
certSign.selectCert=Select Your Certificate File (X.509 format, could be .pem or .der):
certSign.selectP12=Select Your PKCS#12 Keystore File (.p12 or .pfx) (Optional, If provided, it should contain your private key and certificate):
certSign.selectJKS=Select Your Java Keystore File (.jks or .keystore):
certSign.certType=Certificate Type
certSign.password=Enter Your Keystore or Private Key Password (If Any):
certSign.showSig=Show Signature
certSign.reason=Reason
certSign.location=Location
certSign.name=Name
certSign.showLogo=Show Logo
certSign.submit=Sign PDF
certSign.title=Sertifikatla İmzala
certSign.header=PDF-i Sertifikatınızla İmzalayın (İşlənilir)
certSign.selectPDF=İmzalamaq üçün PDF Faylı seçin:
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=Şəxsi Açar faylınızı seçin (PKCS#8 format, .pem və ya .der ola bilər):
certSign.selectCert=Sertifikat faylınızı seçin (X.509 format, .pem və ya .der ola bilər):
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=Java Keystore Faylınızı seçin (.jks və ya .keystore):
certSign.certType=Sertifikat Tipi
certSign.password=Keystore və ya Şəxsi Açar daxil edin (Əgər varsa):
certSign.showSig=İmzanı Göstər
certSign.reason=Səbəb
certSign.location=Məkan
certSign.name=Ad
certSign.showLogo=Loqonu Göstər
certSign.submit=PDF-i İmzala
#removeCertSign
@@ -765,13 +765,13 @@ removeCertSign.submit=İmzanı silin
#removeBlanks
removeBlanks.title=Remove Blanks
removeBlanks.header=Remove Blank Pages
removeBlanks.threshold=Pixel Whiteness Threshold:
removeBlanks.thresholdDesc=Threshold for determining how white a white pixel must be to be classed as 'White'. 0 = Black, 255 pure white.
removeBlanks.whitePercent=White Percent (%):
removeBlanks.whitePercentDesc=Percent of page that must be 'white' pixels to be removed
removeBlanks.submit=Remove Blanks
removeBlanks.title=Boş Səhifələri Sil
removeBlanks.header=Boş SƏhifələri Silir
removeBlanks.threshold=Minimal Piksel Bəyazlığı:
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=Bəyaz Faizi (%):
removeBlanks.whitePercentDesc=Silinmək üçün səhifənin neçə faizi "ağ" piksellərdən təşkil olunmalıdır
removeBlanks.submit=Boş Səhifələri Sil
#removeAnnotations
@@ -781,16 +781,16 @@ removeAnnotations.submit=Sil
#compare
compare.title=Compare
compare.header=Compare PDFs
compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Color 2:
compare.document.1=Document 1
compare.document.2=Document 2
compare.submit=Compare
compare.complex.message=One or both of the provided documents are large files, accuracy of comparison may be reduced
compare.large.file.message=One or Both of the provided documents are too large to process
compare.no.text.message=One or both of the selected PDFs have no text content. Please choose PDFs with text for comparison.
compare.title=Müqayisə Et
compare.header=PDF-ləri Müqayisə Et
compare.highlightColor.1=Önə Çıxarma Rəngi 1:
compare.highlightColor.2=Önə Çıxarma Rəngi 2:
compare.document.1=Sənəd 1
compare.document.2=Sənəd 2
compare.submit=Müqayisə Et
compare.complex.message=Fayllardan biri və ya ikisi də böyük fayldır. Müqayisə effektivliyi azala bilər.
compare.large.file.message=Fayllardan biri və ya ikisi də işləmək üçün çox böyükdür.
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.title=Kitabları və Komiksləri PDF-ə
@@ -818,12 +818,12 @@ sign.save=İmzanı yadda Saxla
sign.personalSigs=Şəxsi İmzalar
sign.sharedSigs=Paylaşılan İmzalar
sign.noSavedSigs=Saxlanmış imza tapılmadı
sign.addToAll=Add to all pages
sign.delete=Delete
sign.first=First page
sign.last=Last page
sign.next=Next page
sign.previous=Previous page
sign.addToAll=Bütün səhiflərə əlavə et
sign.delete=Sil
sign.first=İlk səhifə
sign.last=Son səhifə
sign.next=Növbəti səhifə
sign.previous=Əvvəlki səhifə
#repair
repair.title=Bərpa Et
@@ -839,37 +839,37 @@ flatten.submit=Düzləşdirin
#ScannerImageSplit
ScannerImageSplit.selectText.1=Angle Threshold:
ScannerImageSplit.selectText.2=Sets the minimum absolute angle required for the image to be rotated (default: 10).
ScannerImageSplit.selectText.3=Tolerance:
ScannerImageSplit.selectText.4=Determines the range of color variation around the estimated background color (default: 30).
ScannerImageSplit.selectText.5=Minimum Area:
ScannerImageSplit.selectText.6=Sets the minimum area threshold for a photo (default: 10000).
ScannerImageSplit.selectText.7=Minimum Contour Area:
ScannerImageSplit.selectText.8=Sets the minimum contour area threshold for a photo
ScannerImageSplit.selectText.9=Border Size:
ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1).
ScannerImageSplit.info=Python is not installed. It is required to run.
ScannerImageSplit.selectText.1=Bucaq Aşağı Limiti:
ScannerImageSplit.selectText.2=Şəklin fırladılması üçün lazım olan minimal mütləq bucağı təyin edir (defolt: 10).
ScannerImageSplit.selectText.3=Rəng Toleransı:
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=Minimal Sahə:
ScannerImageSplit.selectText.6=Foto üçün minimal sahənin aşağı limitini təyin edir (defolt: 10000).
ScannerImageSplit.selectText.7=Minimal Kontur Sahəsi:
ScannerImageSplit.selectText.8=Fotonun kontur sahəsi üçün minimal aşağı limiti təyin edir
ScannerImageSplit.selectText.9=Sərhəd Ölçüsü:
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 yüklənməyib. İşə salmaq üçün Python lazımdır.
#OCR
ocr.title=OCR / Scan Cleanup
ocr.header=Cleanup Scans / OCR (Optical Character Recognition)
ocr.selectText.1=Select languages that are to be detected within the PDF (Ones listed are the ones currently detected):
ocr.selectText.2=Produce text file containing OCR text alongside the OCR'ed PDF
ocr.selectText.3=Correct pages were scanned at a skewed angle by rotating them back into place
ocr.selectText.4=Clean page so its less likely that OCR will find text in background noise. (No output change)
ocr.selectText.5=Clean page so its less likely that OCR will find text in background noise, maintains cleanup in output.
ocr.selectText.6=Ignores pages that have interactive text on them, only OCRs pages that are images
ocr.selectText.7=Force OCR, will OCR Every page removing all original text elements
ocr.selectText.8=Normal (Will error if PDF contains text)
ocr.selectText.9=Additional Settings
ocr.selectText.10=OCR Mode
ocr.selectText.11=Remove images after OCR (Removes ALL images, only useful if part of conversion step)
ocr.selectText.12=Render Type (Advanced)
ocr.help=Please read this documentation on how to use this for other languages and/or use not in docker
ocr.credit=This service uses qpdf and Tesseract for OCR.
ocr.submit=Process PDF with OCR
ocr.title=OST (OCR) / Skan Təmizləmə
ocr.header=Skanları Təmizlə / OST (Optik Simvol Tanınması)
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=OST-lənmiş PDF ilə yanaşı daxilində OST edilmiş mətn olan PDF yaradın
ocr.selectText.3=Əyri skan olunmuş səhifələri yerinə fırladaraq düzəldin
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=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=Ü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=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 (PDF-də mətn varsa, xəta verəcək)
ocr.selectText.9=Əlavə Parametrlər
ocr.selectText.10=OST (OCR) Rejimi
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 Tipi (Qabaqcıl)
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=Bu servis OST (OCR) üçün "OCRmyPDF" və "Tesseract" istifadə edir.
ocr.submit=PDF-i OST ilə işlə
#extractImages
@@ -890,15 +890,15 @@ fileToPDF.submit=PDF-ə Çevir
#compress
compress.title=Compress
compress.header=Compress PDF
compress.credit=This service uses qpdf for PDF Compress/Optimisation.
compress.selectText.1=Manual Mode - From 1 to 4
compress.selectText.2=Optimization level:
compress.selectText.3=4 (Terrible for text images)
compress.selectText.4=Auto mode - Auto adjusts quality to get PDF to exact size
compress.selectText.5=Expected PDF Size (e.g. 25MB, 10.8MB, 25KB)
compress.submit=Compress
compress.title=Sıxışdır
compress.header=PDF-i Sıxışdır
compress.credit=Bu servis PDF sıxışdırılması/Optimizasiyası üçün Ghostscript istifadə edir.
compress.selectText.1=Manual Mod - 1-dən 4-ə
compress.selectText.2=Optimizasiya səviyyəsi:
compress.selectText.3=4 (Mətn şəkilləri üçün yaxşı deyil)
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=Gözlənilən PDF Ölçüsü (məsələn, 25MB, 10.8MB, 25KB)
compress.submit=Sıxışdır
#Add image
@@ -919,35 +919,35 @@ merge.submit=Birləşdirin
#pdfOrganiser
pdfOrganiser.title=Page Organiser
pdfOrganiser.header=PDF Page Organiser
pdfOrganiser.submit=Rearrange Pages
pdfOrganiser.mode=Mode
pdfOrganiser.mode.1=Custom Page Order
pdfOrganiser.mode.2=Reverse Order
pdfOrganiser.mode.3=Duplex Sort
pdfOrganiser.mode.4=Booklet Sort
pdfOrganiser.mode.5=Side Stitch Booklet Sort
pdfOrganiser.mode.6=Odd-Even Split
pdfOrganiser.mode.7=Remove First
pdfOrganiser.mode.8=Remove Last
pdfOrganiser.mode.9=Remove First and Last
pdfOrganiser.mode.10=Odd-Even Merge
pdfOrganiser.placeholder=(e.g. 1,3,2 or 4-8,2,10-12 or 2n-1)
pdfOrganiser.title=Səhifə Tənzimləyicisi
pdfOrganiser.header=PDF Səhifə Tənzimləyicisi
pdfOrganiser.submit=Səhifələri Yenidən Təşkil Edin
pdfOrganiser.mode=Rejim
pdfOrganiser.mode.1=Fərdi Səhifə Düzülüşü
pdfOrganiser.mode.2=Tərs Düzülüş
pdfOrganiser.mode.3=İkitərəfli Çeşidləmə
pdfOrganiser.mode.4=Kitabça Çeşidləmə
pdfOrganiser.mode.5=Yan Tikiş Kitabçasının Çeşidlənməsi
pdfOrganiser.mode.6=Tək-Cüt Bölünmə
pdfOrganiser.mode.7=Birincini Sil
pdfOrganiser.mode.8=Sonuncunu Sil
pdfOrganiser.mode.9=Birinci və Sonuncunu Sil
pdfOrganiser.mode.10=Tək-Cüt Birləşdirmə
pdfOrganiser.placeholder=(məs., 1,3,2 və ya 4-8,2,10-12 və ya 2n-1)
#multiTool
multiTool.title=PDF Multi Tool
multiTool.header=PDF Multi Tool
multiTool.uploadPrompts=File Name
multiTool.selectAll=Select All
multiTool.deselectAll=Deselect All
multiTool.selectPages=Page Select
multiTool.selectedPages=Selected Pages
multiTool.page=Page
multiTool.deleteSelected=Delete Selected
multiTool.downloadAll=Export
multiTool.downloadSelected=Export Selected
multiTool.title=PDF Multi-Alət
multiTool.header=PDF Multi-Alət
multiTool.uploadPrompts=Fayl Adı
multiTool.selectAll=Hamısını Seç
multiTool.deselectAll=Hamısını Seçməni Ləğv Et
multiTool.selectPages=Səhifə Seçimi
multiTool.selectedPages=Seçilmiş Səhifələr
multiTool.page=Səhifə
multiTool.deleteSelected=Seçilmişi Sil
multiTool.downloadAll=İxrac Et
multiTool.downloadSelected=Seçilmişi İxrac Et
multiTool.insertPageBreak=Insert Page Break
multiTool.addFile=Add File
@@ -957,10 +957,12 @@ multiTool.split=Split
multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
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.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
viewPdf.title=PDF-ə baxın
@@ -982,32 +984,32 @@ rotate.submit=Fırladın
#split-pdfs
split.title=Split PDF
split.header=Split PDF
split.desc.1=The numbers you select are the page number you wish to do a split on
split.desc.2=As such selecting 1,3,7-9 would split a 10 page document into 6 separate PDFS with:
split.desc.3=Document #1: Page 1
split.desc.4=Document #2: Page 2 and 3
split.desc.5=Document #3: Page 4, 5, 6 and 7
split.desc.6=Document #4: Page 8
split.desc.7=Document #5: Page 9
split.desc.8=Document #6: Page 10
split.splitPages=Enter pages to split on:
split.submit=Split
split.title=PDF-i Bölün
split.header=PDF-i Bölün
split.desc.1=Seçdiyiniz Nömrələr Bölmək İstədiyiniz Səhifə Nömrəsidir
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=Sənəd #1: Səhifə 1
split.desc.4=Sənəd #2: Səhifə 2 və 3
split.desc.5=Sənəd #3: Səhifə 4, 5, 6 7
split.desc.6=Sənəd #4: Səhifə 8
split.desc.7=Sənəd #5: Səhifə 9
split.desc.8=Sənəd #6: Səhifə 10
split.splitPages=Bölünəcək Səhifələri Daxil Edin:
split.submit=Bölün
#merge
imageToPDF.title=Image to PDF
imageToPDF.header=Image to PDF
imageToPDF.submit=Convert
imageToPDF.selectLabel=Image Fit Options
imageToPDF.fillPage=Fill Page
imageToPDF.fitDocumentToImage=Fit Page to Image
imageToPDF.maintainAspectRatio=Maintain Aspect Ratios
imageToPDF.selectText.2=Auto rotate PDF
imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple images)
imageToPDF.selectText.4=Merge into single PDF
imageToPDF.selectText.5=Convert to separate PDFs
imageToPDF.title=Şəkli PDF
imageToPDF.header=Şəkli PDF
imageToPDF.submit=Çevir
imageToPDF.selectLabel=Şəkil Uyğunluğu Seçimləri
imageToPDF.fillPage=Səhifəni Doldur
imageToPDF.fitDocumentToImage=Şəklə Uyğun Səhifə
imageToPDF.maintainAspectRatio=Aspekt Nisbətlərini Qoruyun
imageToPDF.selectText.2=PDF-i Avtomatik Fırlat
imageToPDF.selectText.3=Çoxsaylı Fayl Məntiqi (Yalnız Birdən Çox Şəkil İlə İşləyərkən Aktivdir)
imageToPDF.selectText.4=Tək Bir PDF-ə Birləşdir
imageToPDF.selectText.5=Ayrı PDF-lərə Çevirin
#pdfToImage
@@ -1026,97 +1028,97 @@ pdfToImage.info=Python Yüklü Deyil.WebP Çevirməsi Üçün Vacibdir
#addPassword
addPassword.title=Add Password
addPassword.header=Add password (Encrypt)
addPassword.selectText.1=Select PDF to encrypt
addPassword.selectText.2=User Password
addPassword.selectText.3=Encryption Key Length
addPassword.selectText.4=Higher values are stronger, but lower values have better compatibility.
addPassword.selectText.5=Permissions to set (Recommended to be used along with Owner password)
addPassword.selectText.6=Prevent assembly of document
addPassword.selectText.7=Prevent content extraction
addPassword.selectText.8=Prevent extraction for accessibility
addPassword.selectText.9=Prevent filling in form
addPassword.selectText.10=Prevent modification
addPassword.selectText.11=Prevent annotation modification
addPassword.selectText.12=Prevent printing
addPassword.selectText.13=Prevent printing different formats
addPassword.selectText.14=Owner Password
addPassword.selectText.15=Restricts what can be done with the document once it is opened (Not supported by all readers)
addPassword.selectText.16=Restricts the opening of the document itself
addPassword.submit=Encrypt
addPassword.title=Şifr Əlavə Et
addPassword.header=Şifr Əlavə Et (Şifrləmə)
addPassword.selectText.1=Şifrlənəcək PDF-i seç
addPassword.selectText.2=İstifadəçi Şifri
addPassword.selectText.3=Şifrləmə Açarı Uzunluğu
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=Təyin olunacaq icazə (Sahib (Owner) Şifri ilə birgə istifadə olunması tövsiyə olunur.)
addPassword.selectText.6=Sənədin strukturunun dəyişilməsinin qarşısını al
addPassword.selectText.7=Məzmun xaric edilməsinin qarşısını al
addPassword.selectText.8=Əlçatanlıq üçün xaricetmənin qarşısını al
addPassword.selectText.9=Anketin doldurulmasının qarşısını al
addPassword.selectText.10=Modifikasiyanın qarşısını al
addPassword.selectText.11=Sitat modifikasiyasının qarşısını al
addPassword.selectText.12=Çap etmənin qarşısını al
addPassword.selectText.13=Müxtəlif formatların çap edilməsinin qarşısını al
addPassword.selectText.14=Sahib Şifri
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=Sənədin özünün açılmağını limitləndirir
addPassword.submit=Şifrlə
#watermark
watermark.title=Add Watermark
watermark.header=Add Watermark
watermark.selectText.1=Select PDF to add watermark to:
watermark.selectText.2=Watermark Text:
watermark.selectText.3=Font Size:
watermark.selectText.4=Rotation (0-360):
watermark.selectText.5=widthSpacer (Space between each watermark horizontally):
watermark.selectText.6=heightSpacer (Space between each watermark vertically):
watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Watermark Type:
watermark.selectText.9=Watermark Image:
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Add Watermark
watermark.type.1=Text
watermark.type.2=Image
watermark.title=Watermark Əlavə Et
watermark.header=Watermark Əlavə Et
watermark.selectText.1=Watermark əlavə olunacaq PDF-i seç
watermark.selectText.2=Watermark Mətni:
watermark.selectText.3=Şrift Ölçüsü:
watermark.selectText.4=Fırlatma (0-360):
watermark.selectText.5=enBoşluq (Üfuqi olaraq watermark-lar arasındakı məsafə):
watermark.selectText.6=uzunluqBoşluq (Şaquli olaraq watermark-lar arasındakı məsafə):
watermark.selectText.7=Şəffaflıq (0% - 100%):
watermark.selectText.8=Watermark Tipi:
watermark.selectText.9=Watermark Şəkili:
watermark.selectText.10=PDF-i PDF-Şəkil-ə çevir
watermark.submit=Watermark Əlavə Et
watermark.type.1=Mətn
watermark.type.2=Şəkil
#Change permissions
permissions.title=Change Permissions
permissions.header=Change Permissions
permissions.warning=Warning to have these permissions be unchangeable it is recommended to set them with a password via the add-password page
permissions.selectText.1=Select PDF to change permissions
permissions.selectText.2=Permissions to set
permissions.selectText.3=Prevent assembly of document
permissions.selectText.4=Prevent content extraction
permissions.selectText.5=Prevent extraction for accessibility
permissions.selectText.6=Prevent filling in form
permissions.selectText.7=Prevent modification
permissions.selectText.8=Prevent annotation modification
permissions.selectText.9=Prevent printing
permissions.selectText.10=Prevent printing different formats
permissions.submit=Change
permissions.title=İcazələri Dəyişdir
permissions.header=İcazələri Dəyişdir
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=İcazələri Dəyişdirmək Üçün PDF-i Seç
permissions.selectText.2=Tənzimlənmiş İcazələr
permissions.selectText.3=Sənədin Yığılmasının Qarşısını Al
permissions.selectText.4=Məzmunun Çıxarılmasının Qarşısını Al
permissions.selectText.5=Əlçatanlıq Üçün Çıxarılmasının Qarşısını Alın
permissions.selectText.6=Formanın Doldurulmasının Qarşısını Alır
permissions.selectText.7=Modifikasiyanın Qarşısını Al
permissions.selectText.8=Annotasiyanın Dəyişdirilməsinin Qarşısını Almaq
permissions.selectText.9=Çapın Qarşısını Al
permissions.selectText.10=Fərqli Formatlarda Çapın Qarşısını Al
permissions.submit=Dəyiş
#remove password
removePassword.title=Remove password
removePassword.header=Remove password (Decrypt)
removePassword.selectText.1=Select PDF to Decrypt
removePassword.selectText.2=Password
removePassword.submit=Remove
removePassword.title=Şifri Sil
removePassword.header=Şifri Sil (Deşifr)
removePassword.selectText.1=Deşifr Üçün PDF-i Seç
removePassword.selectText.2=Şifr
removePassword.submit=Sil
#changeMetadata
changeMetadata.title=Change Metadata
changeMetadata.header=Change Metadata
changeMetadata.selectText.1=Please edit the variables you wish to change
changeMetadata.selectText.2=Delete all metadata
changeMetadata.selectText.3=Show Custom Metadata:
changeMetadata.author=Author:
changeMetadata.creationDate=Creation Date (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Creator:
changeMetadata.keywords=Keywords:
changeMetadata.modDate=Modification Date (yyyy/MM/dd HH:mm:ss):
changeMetadata.producer=Producer:
changeMetadata.subject=Subject:
changeMetadata.trapped=Trapped:
changeMetadata.selectText.4=Other Metadata:
changeMetadata.selectText.5=Add Custom Metadata Entry
changeMetadata.submit=Change
changeMetadata.title=Metadata-nı Dəyiş
changeMetadata.header=Metadata-nı Dəyiş
changeMetadata.selectText.1=Dəyişmək istədiyiniz dəyişənləri redaktə edin
changeMetadata.selectText.2=Bütün Metadata-nı Sil
changeMetadata.selectText.3=Fərdi Metadatanı göstərin:
changeMetadata.author=Müəllif:
changeMetadata.creationDate=Yaradılma Tarixi (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Yaradıcı:
changeMetadata.keywords=Açar Sözlər:
changeMetadata.modDate=Dəyişiklik Tarixi (yyyy/MM/dd HH:mm:ss):
changeMetadata.producer=İstehsalçı:
changeMetadata.subject=Mövzu:
changeMetadata.trapped=Tələ:
changeMetadata.selectText.4=Digər Metadata:
changeMetadata.selectText.5=Xüsusi Metadata girişi əlavə edin
changeMetadata.submit=Dəyiş
#pdfToPDFA
pdfToPDFA.title=PDF To PDF/A
pdfToPDFA.header=PDF To PDF/A
pdfToPDFA.credit=This service uses qpdf for PDF/A conversion
pdfToPDFA.submit=Convert
pdfToPDFA.tip=Currently does not work for multiple inputs at once
pdfToPDFA.outputFormat=Output format
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
pdfToPDFA.title=PDF-i PDF/A-ya
pdfToPDFA.header=PDF-i PDF/A-ya
pdfToPDFA.credit=Bu Servis PDF/A Çevirmək Üçün ghostscript İşlədir
pdfToPDFA.submit=Çevir
pdfToPDFA.tip=Hazırda Birdən Çox Giriş Üçün İşləmir
pdfToPDFA.outputFormat=Çıxış Formatı
pdfToPDFA.pdfWithDigitalSignature=PDF Rəqəmsal İmza Ehtiva Edir.Bu, növbəti addımda silinəcək.
#PDFToWord
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=Ниво на отметка: Изберете нивот
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Разделяне на PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Permetre Duplicats: Si està marcat, permet diversos marcadors a la mateixa pàgina per crear PDFs separats.
splitByChapters.submit=Divideix PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Tillad duplikater: Hvis markeret, tillader det flere bogmærker på samme side til at oprette separate PDF'er.
splitByChapters.submit=Splitter PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Duplikate erlauben: Wenn diese Option aktiviert ist, können mehrere Lesezeichen auf derselben Seite separate PDF Dateien erstellen.
splitByChapters.submit=PDF teilen
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=Επίπεδο Σήμανσης Σκέψης: Επιλέ
splitByChapters.desc.3=Πρόσθεση Metadata: Αν επεξεργαστείται, οι αρχικές metadata του PDF θα προστεθούν σε κάθε διαλυμένο PDF.
splitByChapters.desc.4=Διάλυση Παρόντων Τίτλων Επιπέδου: Αν επεξεργαστείται, επιτρέπει τη δημιουργία αποκοπών PDF με βάση πλήρως καθορισμένους σήμαντες έδρας.
splitByChapters.submit=Διαλύστε το PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!

View File

@@ -958,6 +958,8 @@ multiTool.moveLeft=Move Left
multiTool.moveRight=Move Right
multiTool.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Permitir Duplicados: Si está seleccionado, permite que múltiples marcadores en la misma página creen archivos PDF separados.
splitByChapters.submit=Dividir PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Supprimer
multiTool.dragDropMessage=Page(s) sélectionnées
multiTool.undo=Undo
multiTool.redo=Redo
#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 !
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=लेमैक्स स्तर: विभाजन
splitByChapters.desc.3=मॉडेटरेट का शामिल करें: यदि सत्यापित किया जाता है, प्रारंभिक PDF की मॉडेटरेट को प्रत्येक विभाग PDF में शामिल किया जाएगा।
splitByChapters.desc.4=यादृच्छिक पुनरावृत्ति अनुमोदित: यदि सत्यापित किया जाता है, एक ही पेज पर दोहरे मूल्यांकन पब्लिक पीड़एफ बनाने की संभावना देता है।
splitByChapters.submit=PDF विभाजित
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Izinkan Duplikat: Jika dicentang, mengizinkan beberapa markah pada halaman yang sama untuk membuat PDF terpisah.
splitByChapters.submit=Pecah PDF
#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.delete=Elimina
multiTool.dragDropMessage=Pagina(e) selezionata(e)
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
splitByChapters.submit=Dividi PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=북마크 레벨: 분할에 사용할 북마크 레벨을
splitByChapters.desc.3=메타데이터 포함: 체크하면 각 분할된 PDF에는 원본 PDF의 메타데이터가 포함됩니다.
splitByChapters.desc.4=중복 허용: 중복 북마크가 있는 같은 페이지에 여러 번 분할 PDF를 생성합니다.
splitByChapters.submit=PDF 분할
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Dubbele items toestaan: Als gecijfeld, zorgen multiple boekmarkeersymboolen op dezelfde pagina voor het maken van aparte PDF-bestanden.
splitByChapters.submit=PDF splitsen
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Permitir Cópias: Se marcado, habilita vários marcadores na mesma página para criar PDFs separados.
splitByChapters.submit=Dividir PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=Уровень закладки: выберите уро
splitByChapters.desc.3=Включить метаданные: если эта опция отмечена, метаданные исходного PDF будут включены в каждый разбитый PDF.
splitByChapters.desc.4=Позволять дубликаты: если эта опция отмечена, на одной странице могут быть созданы несколько PDF из-за нескольких одинаковых закладок.
splitByChapters.submit=Разделить PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.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
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=ระดับบุคคลที่ได้รับ
splitByChapters.desc.3=รวมข้อมูลเสริม: หากถูกเลือก ข้อมูลเสริมของไฟล์ PDF ที่เดิมจะถูกรวมอยู่ในแต่ละไฟล์ที่แบ่งออก
splitByChapters.desc.4=อนุญาตให้มีการซ้ำ: หากถูกเลือก จะทำให้สามารถสร้างไฟล์ PDF แยกออกมาจากหน้าเดียวกันได้หลายรายการ
splitByChapters.submit=แบ่งไฟล์ PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ 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.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF
#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.delete=Delete
multiTool.dragDropMessage=Page(s) Selected
multiTool.undo=Undo
multiTool.redo=Redo
#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!
@@ -1260,3 +1262,11 @@ splitByChapters.desc.2=書籤層級選擇用於分割的書籤層級0 表
splitByChapters.desc.3=包含中繼資料:如果勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。
splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多個書籤建立獨立的 PDF。
splitByChapters.submit=分割 PDF
#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)
loginAttemptCount: 5 # lock user account after 5 tries; when using e.g. Fail2Ban you can deactivate the function with -1
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
loginMethod: all # '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:
username: '' # initial username 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
clientId: '' # client ID 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
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
provider: google # set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
saml2:
enabled: false # currently in alpha, not recommended for use yet, enableAlphaFunctionality must be set to true
autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
enabled: false # Only enabled for paid enterprise clients (enterpriseEdition.enabled must be true)
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
registrationId: stirling
idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata

View File

@@ -407,7 +407,7 @@
{
"moduleName": "commons-io:commons-io",
"moduleUrl": "https://commons.apache.org/proper/commons-io/",
"moduleVersion": "2.17.0",
"moduleVersion": "2.18.0",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},

View File

@@ -64,7 +64,7 @@ function setupFileInput(chooser) {
dragCounter = 0;
fileInput.dispatchEvent(new Event("change", { bubbles: true }));
fileInput.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: {source: 'drag-drop'} }));
};
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
@@ -81,9 +81,24 @@ function setupFileInput(chooser) {
document.body.addEventListener("drop", dropListener);
$("#" + 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]]);
}
if (!isDragAndDrop) {
let dataTransfer = new DataTransfer();
allFiles.forEach(file => dataTransfer.items.add(file));
element.files = dataTransfer.files;
}
handleFileInputChange(this);
});
this.dispatchEvent(new CustomEvent("file-input-change", { bubbles: true }));
});
function handleFileInputChange(inputElement) {
const files = allFiles;

View File

@@ -3,7 +3,7 @@ let currentSort = {
descending: false,
};
document.getElementById("fileInput-input").addEventListener("change", function () {
document.getElementById("fileInput-input").addEventListener("file-input-change", function () {
var files = this.files;
displayFiles(files);
});

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 {
pageDirection;
pagesContainer;
static selectedPages = []; // Static property shared across all instances
undoManager;
constructor(id) {
constructor(id, undoManager) {
this.pagesContainer = document.getElementById(id);
this.pageDirection = document.documentElement.getAttribute("dir");
this.undoManager = undoManager || new UndoManager();
var styleElement = document.createElement("link");
styleElement.rel = "stylesheet";
styleElement.href = "css/pdfActions.css";
@@ -27,7 +35,8 @@ class PdfActionsManager {
const sibling = imgContainer.previousSibling;
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);
const sibling = imgContainer.nextSibling;
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);
const img = imgContainer.querySelector("img");
this.rotateElement(img, -90);
let rotateCommand = this.rotateElement(img, -90);
this._pushUndoClearRedo(rotateCommand);
}
rotateCWButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target);
const img = imgContainer.querySelector("img");
this.rotateElement(img, 90);
let rotateCommand = this.rotateElement(img, 90);
this._pushUndoClearRedo(rotateCommand);
}
deletePageButtonCallback(e) {
var imgContainer = this.getPageContainer(e.target);
this.pagesContainer.removeChild(imgContainer);
if (this.pagesContainer.childElementCount === 0) {
const filenameInput = document.getElementById("filename-input");
const filenameParagraph = document.getElementById("filename");
const downloadBtn = document.getElementById("export-button");
let imgContainer = this.getPageContainer(e.target);
let deletePageCommand = new DeletePageCommand(
imgContainer,
this.pagesContainer
);
deletePageCommand.execute();
filenameInput.disabled = true;
filenameInput.value = "";
filenameParagraph.innerText = "";
downloadBtn.disabled = true;
}
this._pushUndoClearRedo(deletePageCommand);
}
insertFileButtonCallback(e) {
@@ -81,7 +92,15 @@ class PdfActionsManager {
splitFileButtonCallback(e) {
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 }) {
@@ -159,25 +178,10 @@ class PdfActionsManager {
selectCheckbox.onchange = () => {
const pageNumber = Array.from(div.parentNode.children).indexOf(div) + 1;
if (selectCheckbox.checked) {
//adds to array of selected pages
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);
}
}
let selectPageCommand = new SelectPageCommand(pageNumber, selectCheckbox);
selectPageCommand.execute();
if (window.selectedPages.length > 0 && !window.selectPage) {
window.toggleSelectPageVisibility();
}
if (window.selectedPages.length == 0 && window.selectPage) {
window.toggleSelectPageVisibility();
}
window.updateSelectedPagesDisplay();
this._pushUndoClearRedo(selectPageCommand);
};
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 {
fileName;
pagesContainer;
pagesContainerWrapper;
pdfAdapters;
downloadLink;
undoManager;
constructor(id, wrapperId, pdfAdapters) {
constructor(id, wrapperId, pdfAdapters, undoManager) {
this.pagesContainer = document.getElementById(id);
this.pagesContainerWrapper = document.getElementById(wrapperId);
this.downloadLink = null;
@@ -31,6 +38,8 @@ class PdfContainer {
this.removeAllElements = this.removeAllElements.bind(this);
this.resetPages = this.resetPages.bind(this);
this.undoManager = undoManager || new UndoManager();
this.pdfAdapters = pdfAdapters;
this.pdfAdapters.forEach((adapter) => {
@@ -58,6 +67,33 @@ class PdfContainer {
window.removeAllElements = this.removeAllElements;
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 downloadBtn = document.getElementById("export-button");
@@ -68,32 +104,28 @@ class PdfContainer {
downloadBtn.disabled = true;
}
movePageTo(startElement, endElement, scrollTo = false) {
const childArray = Array.from(this.pagesContainer.childNodes);
const startIndex = childArray.indexOf(startElement);
const endIndex = childArray.indexOf(endElement);
// Check & remove page number elements here too if they exist because Firefox doesn't fire the relevant event on page move.
const pageNumberElement = startElement.querySelector(".page-number");
if (pageNumberElement) {
startElement.removeChild(pageNumberElement);
}
this.pagesContainer.removeChild(startElement);
if (!endElement) {
this.pagesContainer.append(startElement);
movePageTo(startElement, endElement, scrollTo = false, moveUp = false) {
let movePageCommand;
if (moveUp) {
movePageCommand = new MovePageUpCommand(
startElement,
endElement,
this.pagesContainer,
this.pagesContainerWrapper,
scrollTo
);
} else {
this.pagesContainer.insertBefore(startElement, endElement);
movePageCommand = new MovePageDownCommand(
startElement,
endElement,
this.pagesContainer,
this.pagesContainerWrapper,
scrollTo
);
}
if (scrollTo) {
const { width } = startElement.getBoundingClientRect();
const vector = endIndex !== -1 && startIndex > endIndex ? 0 - width : width;
this.pagesContainerWrapper.scroll({
left: this.pagesContainerWrapper.scrollLeft + vector,
});
}
movePageCommand.execute();
return movePageCommand;
}
addFiles(nextSiblingElement, blank = false) {
@@ -165,56 +197,22 @@ class PdfContainer {
async addFilesBlank(nextSiblingElement) {
const pdfContent = `
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
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);
let doc = await PDFLib.PDFDocument.create();
let docBytes = await doc.save();
const url = URL.createObjectURL(new Blob([docBytes], { type: 'application/pdf' }));
const renderer = await this.toRenderer(url);
await this.addPdfFile(renderer, doc, nextSiblingElement);
}
rotateElement(element, deg) {
var lastTransform = element.style.rotate;
if (!lastTransform) {
lastTransform = "0";
}
const lastAngle = parseInt(lastTransform.replace(/[^\d-]/g, ""));
const newAngle = lastAngle + deg;
element.style.rotate = newAngle + "deg";
let rotateCommand = new RotateElementCommand(element, deg);
rotateCommand.execute();
return rotateCommand;
}
async addPdfFile(renderer, pdfDocument, nextSiblingElement) {
@@ -309,6 +307,7 @@ class PdfContainer {
}
rotateAll(deg) {
let elementsToRotate = [];
for (let i = 0; i < this.pagesContainer.childNodes.length; i++) {
const child = this.pagesContainer.children[i];
if (!child) continue;
@@ -320,8 +319,13 @@ class PdfContainer {
const img = child.querySelector("img");
if (!img) continue;
this.rotateElement(img, deg);
elementsToRotate.push(img);
}
let rotateAllCommand = new RotateAllCommand(elementsToRotate, deg);
rotateAllCommand.execute();
this.undoManager.pushUndoClearRedo(rotateAllCommand);
}
removeAllElements(){
@@ -336,34 +340,13 @@ class PdfContainer {
deleteSelected() {
window.selectedPages.sort((a, b) => a - b);
let deletions = 0;
let removeSelectedCommand = new RemoveSelectedCommand(
this.pagesContainer,
window.selectedPages,
this.updatePageNumbersAndCheckboxes
);
window.selectedPages.forEach((pageIndex) => {
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"));
this.undoManager.pushUndoClearRedo(removeSelectedCommand);
}
toggleSelectAll() {
@@ -531,34 +514,17 @@ class PdfContainer {
splitAll() {
const allPages = this.pagesContainer.querySelectorAll(".page-container");
let splitAllCommand = new SplitAllCommand(
allPages,
window.selectPage,
window.selectedPages,
"split-before"
);
splitAllCommand.execute();
if (!window.selectPage) {
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");
}
});
this.undoManager.pushUndoClearRedo(splitAllCommand);
}
async splitPDF(baseDocBytes, splitters) {
const baseDocument = await PDFLib.PDFDocument.load(baseDocBytes);
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

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

View File

@@ -59,7 +59,7 @@
<link rel="stylesheet" th:href="@{'/css/fileSelect.css'}" th:if="${currentPage != 'home'}">
<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>

View File

@@ -156,7 +156,7 @@
resultDiv2.innerHTML = loading;
// 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

View File

@@ -57,6 +57,27 @@
</span>
<span class="btn-tooltip" th:text="#{multiTool.insertPageBreak}"></span>
</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"
onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
@@ -139,7 +160,9 @@
split: '[[#{multiTool.split}]]',
addFile: '[[#{multiTool.addFile}]]',
insertPageBreak:'[[#{multiTool.insertPageBreak}]]',
dragDropMessage:'[[#{multiTool.dragDropMessage}]]'
dragDropMessage:'[[#{multiTool.dragDropMessage}]]',
undo: '[[#{multiTool.undo}]]',
redo: '[[#{multiTool.redo}]]'
};
@@ -155,17 +178,20 @@
});
</script>
<script type="module">
import { UndoManager } from './js/multitool/UndoManager.js';
import PdfContainer from './js/multitool/PdfContainer.js';
import DragDropManager from "./js/multitool/DragDropManager.js";
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
import FileDragManager from './js/multitool/fileInput.js';
// enables drag and drop
var undoManager = new UndoManager();
const dragDropManager = new DragDropManager('drag-container', 'pages-container');
// enables image highlight on click
const imageHighlighter = new ImageHighlighter('image-highlighter');
// 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();
// Scroll the wrapper horizontally
@@ -178,10 +204,23 @@
imageHighlighter,
pdfActionsManager,
fileDragManager
]
],
undoManager
)
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>
</body>

12
test.sh
View File

@@ -73,8 +73,8 @@ main() {
# 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 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 -f ./Dockerfile .
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
# Test each configuration
#run_tests "Stirling-PDF-Ultra-Lite" "./exampleYmlFiles/docker-compose-latest-ultra-lite.yml"
@@ -93,9 +93,9 @@ main() {
# 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 frooodle/s-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 -f ./Dockerfile .
# 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 stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat .
# Test each configuration with security
@@ -103,7 +103,7 @@ main() {
#docker-compose -f "./exampleYmlFiles/docker-compose-latest-ultra-lite-security.yml" down
# run_tests "Stirling-PDF-Security" "./exampleYmlFiles/docker-compose-latest-security.yml"
# docker-compose -f "./exampleYmlFiles/docker-compose-latest-security.yml" down
run_tests "Stirling-PDF-Security-Fat" "./exampleYmlFiles/docker-compose-latest-fat-security.yml"
if [ $? -eq 0 ]; then
cd cucumber