Compare commits

..

31 Commits

Author SHA1 Message Date
Anthony Stirling
a8709c25c0 Update build.gradle 2024-10-14 22:31:55 +01:00
Anthony Stirling
261e69f3ca commit 2024-10-14 22:15:37 +01:00
Anthony Stirling
addb999301 Update home.html 2024-10-14 20:43:27 +01:00
Anthony Stirling
496fcad698 csrf fix #1997 2024-10-07 11:50:07 +01:00
a
daf4f49050 Merge branch 'Frooodle/license' of git@github.com:Stirling-Tools/Stirling-PDF.git into Frooodle/license 2024-10-07 11:01:35 +01:00
Anthony Stirling
2cdad2f9ff remove pattern matching for now 2024-10-07 11:01:22 +01:00
Anthony Stirling
4c4613cfec Merge branch 'main' into Frooodle/license 2024-10-07 10:45:08 +01:00
github-actions[bot]
8aa0e85c20 Update translation files (#1945)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-10-07 10:41:37 +01:00
Anthony Stirling
1129487aaf fixes 2024-10-06 23:32:28 +01:00
Anthony Stirling
378d0f8afb remove calibre 2024-10-06 23:05:51 +01:00
Anthony Stirling
a12de218c0 test 2024-10-06 16:12:48 +01:00
Anthony Stirling
1ba271b0f4 zip 2024-10-06 00:08:30 +01:00
Anthony Stirling
6ccc29d0a7 files 2024-10-06 00:07:35 +01:00
Anthony Stirling
eee2f5c666 readd html, add usercount to license 2024-10-06 00:07:23 +01:00
Anthony Stirling
e660237e28 fingerprint 2024-10-05 17:43:36 +01:00
Anthony Stirling
83e93688ee format 2024-10-05 15:27:57 +01:00
Anthony Stirling
dedfabd630 Merge remote-tracking branch 'origin/main' into Frooodle/license 2024-10-05 09:21:08 +01:00
Anthony Stirling
3b5b7772a9 disable fingerpritning 2024-10-05 09:15:43 +01:00
Anthony Stirling
c59d3ff3e0 all things saml 2024-10-02 23:43:30 +01:00
Anthony Stirling
5832147b30 Merge branch 'main' of git@github.com:Stirling-Tools/Stirling-PDF.git
into main
2024-10-01 13:43:08 +01:00
a
20dc2f60cd changes
Signed-off-by: a <a>
2024-09-25 23:00:49 +01:00
a
b46ccdde44 changes
Signed-off-by: a <a>
2024-09-23 11:03:49 +01:00
a
4fa1b4adb0 Merge remote-tracking branch 'origin/main' into Frooodle/license 2024-09-21 12:39:30 +01:00
a
03158b05e4 log
Signed-off-by: a <a>
2024-09-21 12:39:00 +01:00
a
28c55ca80c navbar enhancements
Signed-off-by: a <a>
2024-09-21 11:59:36 +01:00
a
6ca14edaf1 Merge remote-tracking branch 'origin/main' into Frooodle/license
# Conflicts:
#	src/main/resources/templates/home.html
2024-09-20 13:37:14 +01:00
a
f9677b1fe8 a
Signed-off-by: a <a>
2024-09-20 13:35:37 +01:00
a
04a6ebf515 a
Signed-off-by: a <a>
2024-09-20 13:35:28 +01:00
a
262e2ed47a a
Signed-off-by: a <a>
2024-09-20 13:34:34 +01:00
a
4436759e12 #1869 and ensure naming
Signed-off-by: a <a>
2024-09-20 13:31:55 +01:00
Anthony Stirling
87925ac618 2024-09-15 13:08:12 +01:00
90 changed files with 1945 additions and 3060 deletions

View File

@@ -29,7 +29,7 @@ If you would like to add or modify a translation, please see [How to add new lan
## Docs
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://docs.stirlingpdf.com/](https://docs.stirlingpdf.com/).
Documentation for Stirling-PDF is handled in a separate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
## Fixing Bugs or Adding a New Feature
@@ -41,4 +41,4 @@ If, at any point of time, you have a question, please feel free to ask in the sa
## License
By contributing to this project, you agree that your contributions will be licensed under the [MIT License](LICENSE).
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.

View File

@@ -1,557 +0,0 @@
# Stirling-PDF Developer Guide
## 1. Introduction
Stirling-PDF is a robust, locally hosted web-based PDF manipulation tool. This guide focuses on Docker-based development and testing, which is the recommended approach for working with the full version of Stirling-PDF.
## 2. Project Overview
Stirling-PDF is built using:
- Spring Boot + Thymeleaf
- PDFBox
- LibreOffice
- OcrMyPdf
- HTML, CSS, JavaScript
- Docker
- PDF.js
- PDF-LIB.js
- Lombok
## 3. Development Environment Setup
### Prerequisites
- Docker
- Git
- Java JDK 17 or later
- Gradle 7.0 or later (Included within repo)
### Setup Steps
1. Clone the repository:
```
git clone https://github.com/Stirling-Tools/Stirling-PDF.git
cd Stirling-PDF
```
2. Install Docker and JDK17 if not already installed.
3. Install a recommended Java IDE such as Eclipse, IntelliJ or VSCode
4. Lombok Setup
Stirling-PDF uses Lombok to reduce boilerplate code. Some IDEs, like Eclipse, don't support Lombok out of the box. To set up Lombok in your development environment:
Visit the [Lombok website](https://projectlombok.org/setup/) for installation instructions specific to your IDE.
5. Add environment variable
For local testing you should generally be testing the full 'Security' version of Stirling-PDF to do this you must add the environment flag DOCKER_ENABLE_SECURITY=true to your system and/or IDE build/run step
## 4. Project Structure
```
Stirling-PDF/
├── .github/ # GitHub-specific files (workflows, issue templates)
├── configs/ # Configuration files used by stirling at runtime (generated at runtime)
├── cucumber/ # Cucumber test files
│ ├── features/
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
├── docs/ # Documentation files
├── exampleYmlFiles/ # Example YAML configuration files
├── images/ # Image assets
├── pipeline/ # Pipeline-related files (generated at runtime)
├── scripts/ # Utility scripts
├── src/ # Source code
│ ├── main/
│ │ ├── java/
│ │ │ └── stirling/
│ │ │ └── software/
│ │ │ └── SPDF/
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── model/
│ │ │ ├── repository/
│ │ │ ├── service/
│ │ │ └── utils/
│ │ └── resources/
│ │ ├── static/
│ │ │ ├── css/
│ │ │ ├── js/
│ │ │ └── pdfjs/
│ │ └── templates/
│ └── test/
│ └── java/
│ └── stirling/
│ └── software/
│ └── SPDF/
├── build.gradle # Gradle build configuration
├── Dockerfile # Main Dockerfile
├── Dockerfile-ultra-lite # Dockerfile for ultra-lite version
├── Dockerfile-fat # Dockerfile for fat version
├── docker-compose.yml # Docker Compose configuration
└── test.sh # Test script to deploy all docker versions and run cuke tests
```
## 5. Docker-based Development
Stirling-PDF offers several Docker versions:
- Full: All features included
- Ultra-Lite: Basic PDF operations only
- Fat: Includes additional libraries and fonts predownloaded
### Example Docker Compose Files
Stirling-PDF provides several example Docker Compose files in the `exampleYmlFiles` directory such as :
- `docker-compose-latest.yml`: Latest version without security features
- `docker-compose-latest-security.yml`: Latest version with security features enabled
- `docker-compose-latest-fat-security.yml`: Fat version with security features enabled
These files provide pre-configured setups for different scenarios. For example, here's a snippet from `docker-compose-latest-security.yml`:
```yaml
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- "8080:8080"
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5
```
To use these example files, copy the desired file to your project root and rename it to `docker-compose.yml`, or specify the file explicitly when running Docker Compose:
```bash
docker-compose -f exampleYmlFiles/docker-compose-latest-security.yml up
```
### Building Docker Images
Stirling-PDF uses different Docker images for various configurations. The build process is controlled by environment variables and uses specific Dockerfile variants. Here's how to build the Docker images:
1. Set the security environment variable:
```bash
export DOCKER_ENABLE_SECURITY=false # or true for security-enabled builds
```
2. Build the project with Gradle:
```bash
./gradlew clean build
```
3. Build the Docker images:
For the latest version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
```
For the ultra-lite version:
```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
```
For the fat version (with security enabled):
```bash
export DOCKER_ENABLE_SECURITY=true
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
```
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase
## 6. Testing
### Comprehensive Testing Script
Stirling-PDF provides a `test.sh` script in the root directory. This script builds all versions of Stirling-PDF, checks that each version works, and runs Cucumber tests. It's recommended to run this script before submitting a final pull request.
To run the test script:
```bash
./test.sh
```
This script performs the following actions:
1. Builds all Docker images (full, ultra-lite, fat)
2. Runs each version to ensure it starts correctly
3. Executes Cucumber tests against main version and ensures feature compatibility, in the event these tests fail your PR will not be merged
Note: The `test.sh` script will run automatically when you raise a PR. However, it's recommended to run it locally first to save resources and catch any issues early.
### Full Testing with Docker
1. Build and run the Docker container per the above instructions:
2. Access the application at `http://localhost:8080` and manually test all features developed.
### Local Testing (Java and UI Components)
For quick iterations and development of Java backend, JavaScript, and UI components, you can run and test Stirling-PDF locally without Docker. This approach allows you to work on and verify changes to:
- Java backend logic
- RESTful API endpoints
- JavaScript functionality
- User interface components and styling
- Thymeleaf templates
To run Stirling-PDF locally:
1. Compile and run the project using built in IDE methods or by running:
```
./gradlew bootRun
```
2. Access the application at `http://localhost:8080` in your web browser.
3. Manually test the features you're working on through the UI.
4. For API changes, use tools like Postman or curl to test endpoints directly.
Important notes:
- Local testing doesn't include features that depend on external tools like OCRmyPDF, LibreOffice, or Python scripts.
- There are currently no automated unit tests. All testing is done manually through the UI or API calls. (You are welcome to add JUnits!)
- Always verify your changes in the full Docker environment before submitting pull requests, as some integrations and features will only work in the complete setup.
## 7. Contributing
1. Fork the repository on GitHub.
2. Create a new branch for your feature or bug fix.
3. Make your changes and commit them with clear, descriptive messages and ensure any documentation is updated related to your changes.
4. Test your changes thoroughly in the Docker environment.
5. Run the `test.sh` script to ensure all versions build correctly and pass the Cucumber tests:
```bash
./test.sh
```
6. Push your changes to your fork.
7. Submit a pull request to the main repository.
8. See additional [contributing guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
When you raise a PR:
- The `test.sh` script will run automatically against your PR.
- The PR checks will verify versioning and dependency updates.
- Documentation will be automatically updated for dependency changes.
- Security issues will be checked using Snyk and PixeeBot.
Address any issues that arise from these checks before finalizing your pull request.
## 8. API Documentation
API documentation is available at `/swagger-ui/index.html` when running the application. You can also view the latest API documentation [here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/).
## 9. Customization
Stirling-PDF can be customized through environment variables or a `settings.yml` file. Key customization options include:
- Application name and branding
- Security settings
- UI customization
- Endpoint management
When using Docker, pass environment variables using the `-e` flag or in your `docker-compose.yml` file.
Example:
```
docker run -p 8080:8080 -e APP_NAME="My PDF Tool" stirling-pdf:full
```
Refer to the main README for a full list of customization options.
## 10. Language Translations
For managing language translations that affect multiple files, Stirling-PDF provides a helper script:
```bash
/scripts/replace_translation_line.sh
```
This script helps you make consistent replacements across language files.
When contributing translations:
1. Use the helper script for multi-file changes.
2. Ensure all language files are updated consistently.
3. The PR checks will verify consistency in language file updates.
Remember to test your changes thoroughly to ensure they don't break any existing functionality.
# Code examples
### Overview of Thymeleaf
Thymeleaf is a server-side Java HTML template engine. It is used in Stirling-PDF to render dynamic web pages. Thymeleaf integrates heavily with Spring Boot
### Thymeleaf overview
In Stirling-PDF, Thymeleaf is used to create HTML templates that are rendered on the server side. These templates are located in the `src/main/resources/templates` directory. Thymeleaf templates use a combination of HTML and special Thymeleaf attributes to dynamically generate content.
Some examples of this are
```html
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
or
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
```
Where it uses the th:block, th: indicating its a special thymeleaf element to be used serverside in generating the html, and block being the actual element type.
In this case we are inserting the ``navbar`` entry within the ``fragments/navbar.html`` fragment into the ``th:block`` element.
They can be more complex such as
```html
<th:block th:insert="~{fragments/common :: head(title=#{pageExtracter.title}, header=#{pageExtracter.header})}"></th:block>
```
Which is the same as above but passes the parameters title and header into the fragment common.html to be used in its HTML generation
Thymeleaf can also be used to loop through objects or pass things from java side into html side.
```java
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("exampleData", exampleData);
return "new-feature";
}
```
in above example if exampleData is a list of plain java objects of class Person and within it you had id, name, age etc. You can reference it like so
```html
<tbody>
<!-- Use th:each to iterate over the list -->
<tr th:each="person : ${exampleData}">
<td th:text="${person.id}"></td>
<td th:text="${person.name}"></td>
<td th:text="${person.age}"></td>
<td th:text="${person.email}"></td>
</tr>
</tbody>
```
This would generate n entries of tr for each person in exampleData
### Adding a New Feature to the Backend (API)
1. **Create a New Controller:**
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/api` directory.
- Annotate the class with `@RestController` and `@RequestMapping` to define the API endpoint.
- Ensure to add API documentation annotations like `@Tag(name = "General", description = "General APIs")` and `@Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")`.
```java
package stirling.software.SPDF.controller.api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return "NewFeatureResponse"; // This refers to the NewFeatureResponse.html template presenting the user with the generated html from that file when they navigate to /api/v1/new-feature
}
}
```
2. **Define the Service Layer:** (Not required but often useful)
- Create a new service class in the `src/main/java/stirling/software/SPDF/service` directory.
- Implement the business logic for the new feature.
```java
package stirling.software.SPDF.service;
import org.springframework.stereotype.Service;
@Service
public class NewFeatureService {
public String getNewFeatureData() {
// Implement business logic here
return "New Feature Data";
}
}
```
2b. **Integrate the Service with the Controller:**
- Autowire the service class in the controller and use it to handle the API request.
```java
package stirling.software.SPDF.controller.api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import stirling.software.SPDF.service.NewFeatureService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
@RestController
@RequestMapping("/api/v1/new-feature")
@Tag(name = "General", description = "General APIs")
public class NewFeatureController {
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
@Operation(summary = "New Feature", description = "This is a new feature endpoint.")
public String newFeature() {
return newFeatureService.getNewFeatureData();
}
}
```
### Adding a New Feature to the Frontend (UI)
1. **Create a New Thymeleaf Template:**
- Create a new HTML file in the `src/main/resources/templates` directory.
- Use Thymeleaf attributes to dynamically generate content.
- Use `extract-page.html` as a base example for the HTML template, useful to ensure importing of the general layout, navbar and footer.
```html
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{newFeature.title}, header=#{newFeature.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">upload</span>
<span class="tool-header-text" th:text="#{newFeature.header}"></span>
</div>
<form th:action="@{'/api/v1/new-feature'}" method="post" enctype="multipart/form-data">
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=false, accept='application/pdf')}"></div>
<input type="hidden" id="customMode" name="customMode" value="">
<div class="mb-3">
<label for="featureInput" th:text="#{newFeature.prompt}"></label>
<input type="text" class="form-control" id="featureInput" name="featureInput" th:placeholder="#{newFeature.placeholder}" required>
</div>
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{newFeature.submit}"></button>
</form>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>
```
2. **Create a New Controller for the UI:**
- Create a new Java class in the `src/main/java/stirling/software/SPDF/controller/ui` directory.
- Annotate the class with `@Controller` and `@RequestMapping` to define the UI endpoint.
```java
package stirling.software.SPDF.controller.ui;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import stirling.software.SPDF.service.NewFeatureService;
@Controller
@RequestMapping("/new-feature")
public class NewFeatureUIController {
@Autowired
private NewFeatureService newFeatureService;
@GetMapping
public String newFeaturePage(Model model) {
model.addAttribute("newFeatureData", newFeatureService.getNewFeatureData());
return "new-feature";
}
}
```
3. **Update the Navigation Bar:**
- Add a link to the new feature page in the navigation bar.
- Update the `src/main/resources/templates/fragments/navbar.html` file.
```html
<li class="nav-item">
<a class="nav-link" th:href="@{/new-feature}">New Feature</a>
</li>
```
## Adding New Translations to Existing Language Files in Stirling-PDF
When adding a new feature or modifying existing ones in Stirling-PDF, you'll need to add new translation entries to the existing language files. Here's a step-by-step guide:
### 1. Locate Existing Language Files
Find the existing `messages.properties` files in the `src/main/resources` directory. You'll see files like:
- `messages.properties` (default, usually English)
- `messages_en_GB.properties`
- `messages_fr.properties`
- `messages_de.properties`
- etc.
### 2. Add New Translation Entries
Open each of these files and add your new translation entries. For example, if you're adding a new feature called "PDF Splitter",
Use descriptive, hierarchical keys (e.g., `feature.element.description`)
you might add:
```properties
pdfSplitter.title=PDF Splitter
pdfSplitter.description=Split your PDF into multiple documents
pdfSplitter.button.split=Split PDF
pdfSplitter.input.pages=Enter page numbers to split
```
Add these entries to the default GB language file and any others you wish, translating the values as appropriate for each language.
### 3. Use Translations in Thymeleaf Templates
In your Thymeleaf templates, use the `#{key}` syntax to reference the new translations:
```html
<h1 th:text="#{pdfSplitter.title}">PDF Splitter</h1>
<p th:text="#{pdfSplitter.description}">Split your PDF into multiple documents</p>
<input type="text" th:placeholder="#{pdfSplitter.input.pages}">
<button th:text="#{pdfSplitter.button.split}">Split PDF</button>
```
Remember, never hard-code text in your templates or Java code. Always use translation keys to ensure proper localization.

View File

@@ -172,42 +172,42 @@ Stirling PDF currently supports 38!
| Language | Progress |
| ------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![94%](https://geps.dev/progress/94) |
| Basque (Euskara) (eu_ES) | ![57%](https://geps.dev/progress/57) |
| Bulgarian (Български) (bg_BG) | ![99%](https://geps.dev/progress/99) |
| Catalan (Català) (ca_CA) | ![44%](https://geps.dev/progress/44) |
| Croatian (Hrvatski) (hr_HR) | ![87%](https://geps.dev/progress/87) |
| Czech (Česky) (cs_CZ) | ![83%](https://geps.dev/progress/83) |
| Danish (Dansk) (da_DK) | ![91%](https://geps.dev/progress/91) |
| Dutch (Nederlands) (nl_NL) | ![88%](https://geps.dev/progress/88) |
| Arabic (العربية) (ar_AR) | ![97%](https://geps.dev/progress/97) |
| Basque (Euskara) (eu_ES) | ![58%](https://geps.dev/progress/58) |
| Bulgarian (Български) (bg_BG) | ![89%](https://geps.dev/progress/89) |
| Catalan (Català) (ca_CA) | ![46%](https://geps.dev/progress/46) |
| Croatian (Hrvatski) (hr_HR) | ![89%](https://geps.dev/progress/89) |
| Czech (Česky) (cs_CZ) | ![85%](https://geps.dev/progress/85) |
| Danish (Dansk) (da_DK) | ![94%](https://geps.dev/progress/94) |
| Dutch (Nederlands) (nl_NL) | ![91%](https://geps.dev/progress/91) |
| 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) | ![88%](https://geps.dev/progress/88) |
| German (Deutsch) (de_DE) | ![94%](https://geps.dev/progress/94) |
| Greek (Ελληνικά) (el_GR) | ![75%](https://geps.dev/progress/75) |
| Hindi (हिंदी) (hi_IN) | ![72%](https://geps.dev/progress/72) |
| Hungarian (Magyar) (hu_HU) | ![69%](https://geps.dev/progress/69) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![70%](https://geps.dev/progress/70) |
| Irish (Gaeilge) (ga_IE) | ![90%](https://geps.dev/progress/90) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| Greek (Ελληνικά) (el_GR) | ![78%](https://geps.dev/progress/78) |
| Hindi (हिंदी) (hi_IN) | ![74%](https://geps.dev/progress/74) |
| Hungarian (Magyar) (hu_HU) | ![71%](https://geps.dev/progress/71) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![72%](https://geps.dev/progress/72) |
| Irish (Gaeilge) (ga_IE) | ![93%](https://geps.dev/progress/93) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![87%](https://geps.dev/progress/87) |
| Korean (한국어) (ko_KR) | ![77%](https://geps.dev/progress/77) |
| Norwegian (Norsk) (no_NB) | ![90%](https://geps.dev/progress/90) |
| Polish (Polski) (pl_PL) | ![99%](https://geps.dev/progress/99) |
| Portuguese (Português) (pt_PT) | ![72%](https://geps.dev/progress/72) |
| Portuguese Brazilian (Português) (pt_BR) | ![99%](https://geps.dev/progress/99) |
| Romanian (Română) (ro_RO) | ![92%](https://geps.dev/progress/92) |
| Russian (Русский) (ru_RU) | ![77%](https://geps.dev/progress/77) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![72%](https://geps.dev/progress/72) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![84%](https://geps.dev/progress/84) |
| Spanish (Español) (es_ES) | ![93%](https://geps.dev/progress/93) |
| Swedish (Svenska) (sv_SE) | ![92%](https://geps.dev/progress/92) |
| Thai (ไทย) (th_TH) | ![91%](https://geps.dev/progress/91) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![94%](https://geps.dev/progress/94) |
| Ukrainian (Українська) (uk_UA) | ![82%](https://geps.dev/progress/82) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![91%](https://geps.dev/progress/91) |
| Japanese (日本語) (ja_JP) | ![90%](https://geps.dev/progress/90) |
| Korean (한국어) (ko_KR) | ![80%](https://geps.dev/progress/80) |
| Norwegian (Norsk) (no_NB) | ![93%](https://geps.dev/progress/93) |
| Polish (Polski) (pl_PL) | ![87%](https://geps.dev/progress/87) |
| Portuguese (Português) (pt_PT) | ![74%](https://geps.dev/progress/74) |
| Portuguese Brazilian (Português) (pt_BR) | ![97%](https://geps.dev/progress/97) |
| Romanian (Română) (ro_RO) | ![95%](https://geps.dev/progress/95) |
| Russian (Русский) (ru_RU) | ![79%](https://geps.dev/progress/79) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![74%](https://geps.dev/progress/74) |
| Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) |
| Slovakian (Slovensky) (sk_SK) | ![87%](https://geps.dev/progress/87) |
| Spanish (Español) (es_ES) | ![96%](https://geps.dev/progress/96) |
| Swedish (Svenska) (sv_SE) | ![95%](https://geps.dev/progress/95) |
| Thai (ไทย) (th_TH) | ![94%](https://geps.dev/progress/94) |
| Traditional Chinese (繁體中文) (zh_TW) | ![93%](https://geps.dev/progress/93) |
| Turkish (Türkçe) (tr_TR) | ![97%](https://geps.dev/progress/97) |
| Ukrainian (Українська) (uk_UA) | ![85%](https://geps.dev/progress/85) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![94%](https://geps.dev/progress/94) |
## Contributing (creating issues, translations, fixing bugs, etc.)

View File

@@ -16,13 +16,13 @@ ext {
springBootVersion = "3.3.4"
pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7"
imageioVersion = "3.12.0"
imageioVersion = "3.11.0"
lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1"
}
group = "stirling.software"
version = "0.30.1"
version = "0.30.0"
java {
// 17 is lowest but we support and recommend 21
@@ -32,9 +32,11 @@ java {
repositories {
mavenCentral()
maven { url "https://jitpack.io" }
maven { url "https://build.shibboleth.net/nexus/content/repositories/releases/" }
maven {
url "https://build.shibboleth.net/nexus/content/repositories/releases/"
}
maven {
url 'https://build.shibboleth.net/maven/releases'
url "https://build.shibboleth.net/maven/releases/"
}
}
@@ -119,7 +121,7 @@ configurations.all {
}
dependencies {
//security updates
implementation "org.springframework:spring-webmvc:6.1.14"
implementation "org.springframework:spring-webmvc:6.1.13"
implementation("io.github.pixee:java-security-toolkit:1.2.0")
@@ -133,7 +135,7 @@ dependencies {
implementation "org.springframework.boot:spring-boot-starter-thymeleaf:$springBootVersion"
implementation 'com.posthog.java:posthog:1.1.1'
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
@@ -146,20 +148,12 @@ dependencies {
//2.2.x requires rebuild of DB file.. need migration path
runtimeOnly "com.h2database:h2:2.1.214"
// implementation "com.h2database:h2:2.2.224"
constraints {
implementation "org.opensaml:opensaml-core"
implementation "org.opensaml:opensaml-saml-api"
implementation "org.opensaml:opensaml-saml-impl"
}
implementation "org.springframework.security:spring-security-saml2-service-provider"
implementation 'com.coveo:saml-client:5.0.0'
}
testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
// Batik
implementation "org.apache.xmlgraphics:batik-all:1.18"
implementation "org.apache.xmlgraphics:batik-all:1.17"
// TwelveMonkeys
runtimeOnly "com.twelvemonkeys.imageio:imageio-batik:$imageioVersion"
@@ -201,11 +195,11 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.13.6"
implementation "io.micrometer:micrometer-core:1.13.4"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.24.0"
implementation "org.commonmark:commonmark:0.23.0"
implementation "org.commonmark:commonmark-ext-gfm-tables:0.23.0"
// https://mvnrepository.com/artifact/com.bucket4j/bucket4j_jdk17
implementation "com.bucket4j:bucket4j_jdk17-core:8.14.0"
implementation "com.fathzer:javaluator:3.0.5"

View File

@@ -1,5 +1,5 @@
apiVersion: v2
appVersion: 0.30.1
appVersion: 0.29.0
description: locally hosted web application that allows you to perform various operations
on PDF files
home: https://github.com/Stirling-Tools/Stirling-PDF

View File

@@ -1,20 +0,0 @@
#!/bin/bash
# Check if a key was provided
if [ $# -eq 0 ]; then
echo "Please provide a key to remove."
exit 1
fi
key_to_remove="$1"
for file in ../src/main/resources/messages_*.properties; do
# If the key ends with a dot, remove all keys starting with it
if [[ "$key_to_remove" == *. ]]; then
sed -i "/^${key_to_remove//./\\.}/d" "$file"
else
# Otherwise, remove only the exact key match
sed -i "/^${key_to_remove//./\\.}=/d" "$file"
fi
echo "Updated $file"
done

View File

@@ -7,19 +7,20 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Lazy
@Slf4j
public class EEAppConfig {
private static final Logger logger = LoggerFactory.getLogger(EEAppConfig.class);
@Autowired ApplicationProperties applicationProperties;
@Autowired private LicenseKeyChecker licenseKeyChecker;
@Bean(name = "runningEE")
public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getEnterpriseEnabledResult();
return licenseKeyChecker.getEnterpriseEnabledResult();
}
}
}

View File

@@ -33,15 +33,11 @@ public class SPdfApplication {
@Autowired private Environment env;
@Autowired ApplicationProperties applicationProperties;
private static String baseUrlStatic;
private static String serverPortStatic;
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Value("${server.port:8080}")
public void setServerPortStatic(String port) {
if ("auto".equalsIgnoreCase(port)) {
if (port.equalsIgnoreCase("auto")) {
// Use Spring Boot's automatic port assignment (server.port=0)
SPdfApplication.serverPortStatic =
"0"; // This will let Spring Boot assign an available port
@@ -69,13 +65,12 @@ public class SPdfApplication {
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
// Check if the BROWSER_OPEN environment variable is set to true
String browserOpenEnv = env.getProperty("BROWSER_OPEN");
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv);
if (browserOpen) {
try {
String url = baseUrl + ":" + getStaticPort();
String url = "http://localhost:" + getStaticPort();
String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime();
@@ -83,9 +78,9 @@ public class SPdfApplication {
// For Windows
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} else if (os.contains("mac")) {
SystemCommand.runCommand(rt, "open " + url);
rt.exec("open " + url);
} else if (os.contains("nix") || os.contains("nux")) {
SystemCommand.runCommand(rt, "xdg-open " + url);
rt.exec("xdg-open " + url);
}
} catch (Exception e) {
logger.error("Error opening browser: {}", e.getMessage());
@@ -143,18 +138,10 @@ public class SPdfApplication {
private static void printStartupLogs() {
logger.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort();
String url = "http://localhost:" + getStaticPort();
logger.info("Navigate to {}", url);
}
public static String getStaticBaseUrl() {
return baseUrlStatic;
}
public String getNonStaticBaseUrl() {
return baseUrlStatic;
}
public static String getStaticPort() {
return serverPortStatic;
}

View File

@@ -15,7 +15,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
@@ -163,14 +162,12 @@ public class AppConfig {
}
@Bean(name = "analyticsPrompt")
@Scope("request")
public boolean analyticsPrompt() {
return applicationProperties.getSystem().getEnableAnalytics() == null
|| "undefined".equals(applicationProperties.getSystem().getEnableAnalytics());
}
@Bean(name = "analyticsEnabled")
@Scope("request")
public boolean analyticsEnabled() {
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().getEnableAnalytics() != null

View File

@@ -8,7 +8,6 @@ import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import io.micrometer.common.util.StringUtils;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@@ -40,26 +39,4 @@ public class InitialSetup {
applicationProperties.getAutomaticallyGenerated().setKey(secretKey);
}
}
@PostConstruct
public void initLegalUrls() throws IOException {
// Initialize Terms and Conditions
String termsUrl = applicationProperties.getLegal().getTermsAndConditions();
if (StringUtils.isEmpty(termsUrl)) {
String defaultTermsUrl = "https://www.stirlingpdf.com/terms-and-conditions";
GeneralUtils.saveKeyToConfig("legal.termsAndConditions", defaultTermsUrl);
applicationProperties.getLegal().setTermsAndConditions(defaultTermsUrl);
}
// Initialize Privacy Policy
String privacyUrl = applicationProperties.getLegal().getPrivacyPolicy();
if (StringUtils.isEmpty(privacyUrl)) {
String defaultPrivacyUrl = "https://www.stirlingpdf.com/privacy-policy";
GeneralUtils.saveKeyToConfig("legal.privacyPolicy", defaultPrivacyUrl);
applicationProperties.getLegal().setPrivacyPolicy(defaultPrivacyUrl);
}
}
}

View File

@@ -1,237 +1,27 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import com.coveo.saml.SamlClient;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
@AllArgsConstructor
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
if (!response.isCommitted()) {
// Handle user logout due to disabled account
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
return;
}
// Handle OAuth2 authentication error
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userAlreadyExistsWeb");
return;
}
if (authentication != null) {
// Handle SAML2 logout redirection
if (authentication instanceof Saml2Authentication) {
getRedirect_saml2(request, response, authentication);
return;
}
// Handle OAuth2 logout redirection
else if (authentication instanceof OAuth2AuthenticationToken) {
getRedirect_oauth2(request, response, authentication);
return;
}
// Handle Username/Password logout
else if (authentication instanceof UsernamePasswordAuthenticationToken) {
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
// Handle unknown authentication types
else {
log.error(
"authentication class unknown: "
+ authentication.getClass().getSimpleName());
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
} else {
// Redirect to login page after logout
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
return;
}
}
}
// Redirect for SAML2 authentication logout
private void getRedirect_saml2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
String registrationId = samlConf.getRegistrationId();
Saml2Authentication samlAuthentication = (Saml2Authentication) authentication;
CustomSaml2AuthenticatedPrincipal principal =
(CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal();
String nameIdValue = principal.getName();
try {
// Read certificate from the resource
Resource certificateResource = samlConf.getSpCert();
X509Certificate certificate = CertificateUtils.readCertificate(certificateResource);
List<X509Certificate> certificates = new ArrayList<>();
certificates.add(certificate);
// Construct URLs required for SAML configuration
String serverUrl =
SPdfApplication.getStaticBaseUrl() + ":" + SPdfApplication.getStaticPort();
String relyingPartyIdentifier =
serverUrl + "/saml2/service-provider-metadata/" + registrationId;
String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId;
String idpUrl = samlConf.getIdpSingleLogoutUrl();
String idpIssuer = samlConf.getIdpIssuer();
// Create SamlClient instance for SAML logout
SamlClient samlClient =
new SamlClient(
relyingPartyIdentifier,
assertionConsumerServiceUrl,
idpUrl,
idpIssuer,
certificates,
SamlClient.SamlIdpBinding.POST);
// Read private key for service provider
Resource privateKeyResource = samlConf.getPrivateKey();
RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource);
// Set service provider keys for the SamlClient
samlClient.setSPKeys(certificate, privateKey);
// Redirect to identity provider for logout
samlClient.redirectToIdentityProvider(response, null, nameIdValue);
} catch (Exception e) {
log.error(nameIdValue, e);
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
}
}
// Redirect for OAuth2 authentication logout
private void getRedirect_oauth2(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
// Get OAuth2 provider details from configuration
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
// Handle different error scenarios during logout
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
if (request.getParameter("userIsDisabled") != null) {
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=userIsDisabled");
return;
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
// Redirect based on OAuth2 provider
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
// Sanitize input to avoid potential security vulnerabilities
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
getRedirectStrategy().sendRedirect(request, response, "/login?logout=true");
}
}

View File

@@ -1,36 +1,22 @@
package stirling.software.SPDF.config.security;
import java.security.cert.X509Certificate;
import java.util.*;
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.Lazy;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.ClientRegistrations;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
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.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.web.SecurityFilterChain;
@@ -42,20 +28,13 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
import stirling.software.SPDF.config.security.saml2.CertificateUtils;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
import stirling.software.SPDF.config.security.saml.ConvertResponseToAuthentication;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationFailureHandler;
import stirling.software.SPDF.config.security.saml.CustomSAMLAuthenticationSuccessHandler;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration
@@ -66,6 +45,12 @@ public class SecurityConfiguration {
@Autowired private CustomUserDetailsService userDetailsService;
@Autowired(required = false)
private GrantedAuthoritiesMapper userAuthoritiesMapper;
@Autowired(required = false)
private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
@@ -86,8 +71,11 @@ public class SecurityConfiguration {
@Autowired private FirstLoginFilter firstLoginFilter;
@Autowired private SessionPersistentRegistry sessionRegistry;
@Autowired private ConvertResponseToAuthentication convertResponseToAuthentication;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authenticationManager(authenticationManager(http));
if (loginEnabledValue) {
http.addFilterBefore(
@@ -106,116 +94,128 @@ public class SecurityConfiguration {
.sessionRegistry(sessionRegistry)
.expiredUrl("/login?logout=true"));
http.authenticationProvider(daoAuthenticationProvider());
http.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()));
http.logout(
logout ->
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(
new CustomLogoutSuccessHandler(applicationProperties))
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"));
http.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
);
http.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.permitAll())
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.logout(
logout ->
logout.logoutRequestMatcher(
new AntPathRequestMatcher("/logout"))
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.invalidateHttpSession(true) // Invalidate session
.deleteCookies("JSESSIONID", "remember-me"))
.rememberMe(
rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
.key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(1209600) // 2 weeks
)
.authorizeHttpRequests(
authz ->
authz.requestMatchers(
req -> {
String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath.length())
: uri;
// Remove the context path from the URI
String trimmedUri =
uri.startsWith(contextPath)
? uri.substring(
contextPath
.length())
: uri;
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith("/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated());
// Handle User/Password Logins
if (applicationProperties.getSecurity().isUserPass()) {
http.formLogin(
formLogin ->
formLogin
.loginPage("/login")
.successHandler(
new CustomAuthenticationSuccessHandler(
loginAttemptService, userService))
.failureHandler(
new CustomAuthenticationFailureHandler(
loginAttemptService, userService))
.defaultSuccessUrl("/")
.permitAll());
}
return trimmedUri.startsWith("/login")
|| trimmedUri.startsWith("/oauth")
|| trimmedUri.startsWith("/saml2")
|| trimmedUri.endsWith(".svg")
|| trimmedUri.startsWith(
"/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/fonts/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated());
// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().isOauth2Activ()) {
if (applicationProperties.getSecurity().getOauth2() != null
&& applicationProperties.getSecurity().getOauth2().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.oauth2Login(
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
userAuthoritiesMapper()))
.permitAll());
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Activ() && applicationProperties.getSystem().getEnableAlphaFunctionality()) {
http.authenticationProvider(samlAuthenticationProvider());
http.saml2Login(
saml2 ->
saml2.loginPage("/saml2")
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
.successHandler(
new CustomSaml2AuthenticationSuccessHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomSaml2AuthenticationFailureHandler())
.permitAll())
new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
userAuthoritiesMapper)))
.logout(
logout ->
logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler(
applicationProperties)));
}
// Handle SAML
if (applicationProperties.getSecurity().getSaml() != null
&& applicationProperties.getSecurity().getSaml().getEnabled()
&& !applicationProperties
.getSecurity()
.getLoginMethod()
.equalsIgnoreCase("normal")) {
http.saml2Login(
saml2 -> {
saml2.loginPage("/saml2")
.relyingPartyRegistrationRepository(
relyingPartyRegistrationRepository)
.successHandler(
new CustomSAMLAuthenticationSuccessHandler(
loginAttemptService,
userService,
applicationProperties))
.failureHandler(
new CustomSAMLAuthenticationFailureHandler());
})
.addFilterBefore(
userAuthenticationFilter, Saml2WebSsoAuthenticationFilter.class);
}
@@ -231,234 +231,39 @@ public class SecurityConfiguration {
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
name = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(
new CustomSaml2ResponseAuthenticationConverter(userService));
authenticationProvider.setResponseAuthenticationConverter(convertResponseToAuthentication);
return authenticationProvider;
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
List<ClientRegistration> registrations = new ArrayList<>();
githubClientRegistration().ifPresent(registrations::add);
oidcClientRegistration().ifPresent(registrations::add);
googleClientRegistration().ifPresent(registrations::add);
keycloakClientRegistration().ifPresent(registrations::add);
if (registrations.isEmpty()) {
log.error("At least one OAuth2 provider must be configured");
System.exit(1);
}
return new InMemoryClientRegistrationRepository(registrations);
}
private Optional<ClientRegistration> googleClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GoogleProvider google = client.getGoogle();
return google != null && google.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(google.getName())
.clientId(google.getClientId())
.clientSecret(google.getClientSecret())
.scope(google.getScopes())
.authorizationUri(google.getAuthorizationuri())
.tokenUri(google.getTokenuri())
.userInfoUri(google.getUserinfouri())
.userNameAttributeName(google.getUseAsUsername())
.clientName(google.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + google.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> keycloakClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
KeycloakProvider keycloak = client.getKeycloak();
return keycloak != null && keycloak.isSettingsValid()
? Optional.of(
ClientRegistrations.fromIssuerLocation(keycloak.getIssuer())
.registrationId(keycloak.getName())
.clientId(keycloak.getClientId())
.clientSecret(keycloak.getClientSecret())
.scope(keycloak.getScopes())
.userNameAttributeName(keycloak.getUseAsUsername())
.clientName(keycloak.getClientName())
.build())
: Optional.empty();
}
private Optional<ClientRegistration> githubClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null || !oauth.getEnabled()) {
return Optional.empty();
}
Client client = oauth.getClient();
if (client == null) {
return Optional.empty();
}
GithubProvider github = client.getGithub();
return github != null && github.isSettingsValid()
? Optional.of(
ClientRegistration.withRegistrationId(github.getName())
.clientId(github.getClientId())
.clientSecret(github.getClientSecret())
.scope(github.getScopes())
.authorizationUri(github.getAuthorizationuri())
.tokenUri(github.getTokenuri())
.userInfoUri(github.getUserinfouri())
.userNameAttributeName(github.getUseAsUsername())
.clientName(github.getClientName())
.redirectUri("{baseUrl}/login/oauth2/code/" + github.getName())
.authorizationGrantType(
org.springframework.security.oauth2.core
.AuthorizationGrantType.AUTHORIZATION_CODE)
.build())
: Optional.empty();
}
private Optional<ClientRegistration> oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth == null
|| oauth.getIssuer() == null
|| oauth.getIssuer().isEmpty()
|| oauth.getClientId() == null
|| oauth.getClientId().isEmpty()
|| oauth.getClientSecret() == null
|| oauth.getClientSecret().isEmpty()
|| oauth.getScopes() == null
|| oauth.getScopes().isEmpty()
|| oauth.getUseAsUsername() == null
|| oauth.getUseAsUsername().isEmpty()) {
return Optional.empty();
}
return Optional.of(
ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build());
}
// @Bean
// public AuthenticationProvider daoAuthenticationProvider() {
// DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// provider.setUserDetailsService(userDetailsService); // UserDetailsService
// provider.setPasswordEncoder(passwordEncoder()); // PasswordEncoder
// return provider;
// }
@Bean
@ConditionalOnProperty(
name = "security.saml2.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception {
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder authenticationManagerBuilder =
http.getSharedObject(AuthenticationManagerBuilder.class);
SAML2 samlConf = applicationProperties.getSecurity().getSaml2();
// authenticationManagerBuilder =
// authenticationManagerBuilder.authenticationProvider(
// daoAuthenticationProvider()); // Benutzername/Passwort
Resource privateKeyResource = samlConf.getPrivateKey();
Resource certificateResource = samlConf.getSpCert();
Saml2X509Credential signingCredential =
new Saml2X509Credential(
CertificateUtils.readPrivateKey(privateKeyResource),
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))
.assertingPartyDetails(
(details) ->
details.entityId(samlConf.getIdpIssuer())
.singleSignOnServiceLocation(
samlConf.getIdpSingleLoginUrl())
.verificationX509Credentials(
(c) -> c.add(verificationCredential))
.wantAuthnRequestsSigned(true))
.build();
return new InMemoryRelyingPartyRegistrationRepository(rp);
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOauth2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
if (applicationProperties.getSecurity().getSaml() != null
&& applicationProperties.getSecurity().getSaml().getEnabled()) {
authenticationManagerBuilder.authenticationProvider(
samlAuthenticationProvider()); // SAML
}
return authenticationManagerBuilder.build();
}
@Bean

View File

@@ -22,7 +22,6 @@ import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
import stirling.software.SPDF.model.User;
@@ -112,9 +111,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter()
.write(
"Authentication required. Please provide a X-API-KEY in request header.\n"
+ "This is found in Settings -> Account Settings -> API Key\n"
+ "Alternatively you can disable authentication if this is unexpected");
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected");
return;
}
}
@@ -127,8 +124,6 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
username = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
username = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
username = (String) principal;
}

View File

@@ -20,7 +20,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.AuthenticationType;
@@ -45,6 +44,9 @@ public class UserService implements UserServiceInterface {
@Autowired DatabaseBackupInterface databaseBackupHelper;
public long getTotalUserCount() {
return userRepository.count();
}
// Handle OAUTH2 login and user auto creation.
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser)
@@ -336,10 +338,6 @@ public class UserService implements UserServiceInterface {
} else if (principal instanceof OAuth2User) {
OAuth2User oAuth2User = (OAuth2User) principal;
usernameP = oAuth2User.getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
CustomSaml2AuthenticatedPrincipal saml2User =
(CustomSaml2AuthenticatedPrincipal) principal;
usernameP = saml2User.getName();
} else if (principal instanceof String) {
usernameP = (String) principal;
}
@@ -359,9 +357,4 @@ public class UserService implements UserServiceInterface {
return principal.toString();
}
}
@Override
public long getTotalUsersCount() {
return userRepository.count();
}
}

View File

@@ -51,7 +51,8 @@ public class CustomOAuth2AuthenticationFailureHandler
}
log.error("OAuth2 Authentication error: " + errorCode);
log.error("OAuth2AuthenticationException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=" + errorCode);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
return;
}
log.error("Unhandled authentication exception", exception);

View File

@@ -75,11 +75,6 @@ public class CustomOAuth2AuthenticationSuccessHandler
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.isUserDisabled(username)) {
getRedirectStrategy()
.sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(

View File

@@ -0,0 +1,122 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.Provider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.UrlUtils;
@Slf4j
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String param = "logout=true";
String registrationId = null;
String issuer = null;
String clientId = null;
if (authentication == null) {
if (request.getParameter("userIsDisabled") != null) {
response.sendRedirect(
request.getContextPath() + "/login?erroroauth=userIsDisabled");
} else {
super.onLogoutSuccess(request, response, authentication);
}
return;
}
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (authentication instanceof OAuth2AuthenticationToken) {
OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication;
registrationId = oauthToken.getAuthorizedClientRegistrationId();
try {
Provider provider = oauth.getClient().get(registrationId);
issuer = provider.getIssuer();
clientId = provider.getClientId();
} catch (UnsupportedProviderException e) {
log.error(e.getMessage());
}
} else {
registrationId = oauth.getProvider() != null ? oauth.getProvider() : "";
issuer = oauth.getIssuer();
clientId = oauth.getClientId();
}
String errorMessage = "";
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if ((errorMessage = request.getParameter("error")) != null) {
param = "error=" + sanitizeInput(errorMessage);
} else if ((errorMessage = request.getParameter("erroroauth")) != null) {
param = "erroroauth=" + sanitizeInput(errorMessage);
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
} else if (request.getParameter("oauth2_admin_blocked_user") != null) {
param = "erroroauth=oauth2_admin_blocked_user";
} else if (request.getParameter("userIsDisabled") != null) {
param = "erroroauth=userIsDisabled";
} else if (request.getParameter("badcredentials") != null) {
param = "error=badcredentials";
}
String redirect_url = UrlUtils.getOrigin(request) + "/login?" + param;
switch (registrationId.toLowerCase()) {
case "keycloak":
// Add Keycloak specific logout URL if needed
String logoutUrl =
issuer
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ clientId
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(redirect_url);
log.info("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "github":
// Add GitHub specific logout URL if needed
String githubLogoutUrl = "https://github.com/logout";
log.info("Redirecting to GitHub logout URL: " + githubLogoutUrl);
response.sendRedirect(githubLogoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
// String googleLogoutUrl =
// "https://accounts.google.com/Logout?continue=https://appengine.google.com/_ah/logout?continue="
// + response.encodeRedirectURL(redirect_url);
log.info("Google does not have a specific logout URL");
// log.info("Redirecting to Google logout URL: " + googleLogoutUrl);
// response.sendRedirect(googleLogoutUrl);
// break;
default:
String defaultRedirectUrl = request.getContextPath() + "/login?" + param;
log.info("Redirecting to default logout URL: " + defaultRedirectUrl);
response.sendRedirect(defaultRedirectUrl);
break;
}
}
private String sanitizeInput(String input) {
return input.replaceAll("[^a-zA-Z0-9 ]", "");
}
}

View File

@@ -0,0 +1,68 @@
package stirling.software.SPDF.config.security.saml;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class ConvertResponseToAuthentication
implements Converter<ResponseToken, Saml2Authentication> {
private final Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup;
public ConvertResponseToAuthentication(
Saml2AuthorityAttributeLookup saml2AuthorityAttributeLookup) {
this.saml2AuthorityAttributeLookup = saml2AuthorityAttributeLookup;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
final Assertion assertion =
CollectionUtils.firstElement(responseToken.getResponse().getAssertions());
final Map<String, List<Object>> attributes =
SamlAssertionUtils.getAssertionAttributes(assertion);
final String registrationId =
responseToken.getToken().getRelyingPartyRegistration().getRegistrationId();
final ScimSaml2AuthenticatedPrincipal principal =
new ScimSaml2AuthenticatedPrincipal(
assertion,
attributes,
saml2AuthorityAttributeLookup.getIdentityMappings(registrationId));
final Collection<? extends GrantedAuthority> assertionAuthorities =
getAssertionAuthorities(
attributes,
saml2AuthorityAttributeLookup.getAuthorityAttribute(registrationId));
return new Saml2Authentication(
principal, responseToken.getToken().getSaml2Response(), assertionAuthorities);
}
private static Collection<? extends GrantedAuthority> getAssertionAuthorities(
final Map<String, List<Object>> attributes, final String authoritiesAttributeName) {
if (attributes == null || attributes.isEmpty()) {
return Collections.emptySet();
}
final List<Object> groups = new ArrayList<>(attributes.get(authoritiesAttributeName));
return groups.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.map(String::toLowerCase)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
}
}

View File

@@ -0,0 +1,51 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSAMLAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof BadCredentialsException) {
log.error("BadCredentialsException", exception);
getRedirectStrategy().sendRedirect(request, response, "/login?error=badcredentials");
return;
}
if (exception instanceof DisabledException) {
log.error("User is deactivated: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?userIsDisabled=true");
return;
}
if (exception instanceof LockedException) {
log.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
return;
}
if (exception instanceof Saml2AuthenticationException) {
log.error("SAML2 Authentication error: ", exception);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?error=saml2AuthenticationError");
return;
}
log.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
}
}

View File

@@ -0,0 +1,108 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
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;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@Slf4j
public class CustomSAMLAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private UserService userService;
private ApplicationProperties applicationProperties;
public CustomSAMLAuthenticationSuccessHandler(
LoginAttemptService loginAttemptService,
UserService userService,
ApplicationProperties applicationProperties) {
this.loginAttemptService = loginAttemptService;
this.userService = userService;
this.applicationProperties = applicationProperties;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
String username = "";
if (principal instanceof OAuth2User) {
OAuth2User oauthUser = (OAuth2User) principal;
username = oauthUser.getName();
} else if (principal instanceof UserDetails) {
UserDetails oauthUser = (UserDetails) principal;
username = oauthUser.getUsername();
} else if (principal instanceof ScimSaml2AuthenticatedPrincipal) {
ScimSaml2AuthenticatedPrincipal samlPrincipal =
(ScimSaml2AuthenticatedPrincipal) principal;
username = samlPrincipal.getName();
}
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
OAUTH2 oAuth = applicationProperties.getSecurity().getOauth2();
if (loginAttemptService.isBlocked(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)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (oAuth.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(contextPath + "/logout?oauth2_admin_blocked_user=true");
return;
}
if (principal instanceof OAuth2User) {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
}
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
}
}

View File

@@ -0,0 +1,38 @@
package stirling.software.SPDF.config.security.saml;
import java.io.IOException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class SAMLLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String redirectUrl = determineTargetUrl(request, response, authentication);
if (response.isCommitted()) {
log.debug("Response has already been committed. Unable to redirect to " + redirectUrl);
return;
}
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
// Default to the root URL
return "/";
}
}

View File

@@ -0,0 +1,7 @@
package stirling.software.SPDF.config.security.saml;
public interface Saml2AuthorityAttributeLookup {
String getAuthorityAttribute(String registrationId);
SimpleScimMappings getIdentityMappings(String registrationId);
}

View File

@@ -0,0 +1,17 @@
package stirling.software.SPDF.config.security.saml;
import org.springframework.stereotype.Component;
@Component
public class Saml2AuthorityAttributeLookupImpl implements Saml2AuthorityAttributeLookup {
@Override
public String getAuthorityAttribute(String registrationId) {
return "authorityAttributeName";
}
@Override
public SimpleScimMappings getIdentityMappings(String registrationId) {
return new SimpleScimMappings();
}
}

View File

@@ -0,0 +1,63 @@
package stirling.software.SPDF.config.security.saml;
import java.time.Instant;
import java.util.*;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.schema.*;
import org.opensaml.saml.saml2.core.Assertion;
public class SamlAssertionUtils {
public static Map<String, List<Object>> getAssertionAttributes(Assertion assertion) {
Map<String, List<Object>> attributeMap = new LinkedHashMap<>();
assertion
.getAttributeStatements()
.forEach(
attributeStatement -> {
attributeStatement
.getAttributes()
.forEach(
attribute -> {
List<Object> attributeValues = new ArrayList<>();
attribute
.getAttributeValues()
.forEach(
xmlObject -> {
Object attributeValue =
getXmlObjectValue(
xmlObject);
if (attributeValue != null) {
attributeValues.add(
attributeValue);
}
});
attributeMap.put(
attribute.getName(), attributeValues);
});
});
return attributeMap;
}
public static Object getXmlObjectValue(XMLObject xmlObject) {
if (xmlObject instanceof XSAny) {
return ((XSAny) xmlObject).getTextContent();
} else if (xmlObject instanceof XSString) {
return ((XSString) xmlObject).getValue();
} else if (xmlObject instanceof XSInteger) {
return ((XSInteger) xmlObject).getValue();
} else if (xmlObject instanceof XSURI) {
return ((XSURI) xmlObject).getURI();
} else if (xmlObject instanceof XSBoolean) {
return ((XSBoolean) xmlObject).getValue().getValue();
} else if (xmlObject instanceof XSDateTime) {
Instant dateTime = ((XSDateTime) xmlObject).getValue();
return (dateTime != null) ? Instant.ofEpochMilli(dateTime.toEpochMilli()) : null;
}
return null;
}
}

View File

@@ -0,0 +1,42 @@
package stirling.software.SPDF.config.security.saml;
import java.security.cert.CertificateException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.registration.RelyingPartyRegistrations;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration
@Slf4j
public class SamlConfig {
@Autowired ApplicationProperties applicationProperties;
@Bean
@ConditionalOnProperty(
value = "security.saml.enabled",
havingValue = "true",
matchIfMissing = false)
public RelyingPartyRegistrationRepository relyingPartyRegistrationRepository()
throws CertificateException {
RelyingPartyRegistration registration =
RelyingPartyRegistrations.fromMetadataLocation(
applicationProperties
.getSecurity()
.getSaml()
.getIdpMetadataLocation())
.entityId(applicationProperties.getSecurity().getSaml().getEntityId())
.registrationId(
applicationProperties.getSecurity().getSaml().getRegistrationId())
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
}

View File

@@ -0,0 +1,89 @@
package stirling.software.SPDF.config.security.saml;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.opensaml.saml.saml2.core.Assertion;
import org.springframework.security.core.AuthenticatedPrincipal;
import org.springframework.util.Assert;
import com.unboundid.scim2.common.types.Email;
import com.unboundid.scim2.common.types.Name;
import com.unboundid.scim2.common.types.UserResource;
public class ScimSaml2AuthenticatedPrincipal implements AuthenticatedPrincipal, Serializable {
private static final long serialVersionUID = 1L;
private final transient UserResource userResource;
public ScimSaml2AuthenticatedPrincipal(
final Assertion assertion,
final Map<String, List<Object>> attributes,
final SimpleScimMappings attributeMappings) {
Assert.notNull(assertion, "assertion cannot be null");
Assert.notNull(assertion.getSubject(), "assertion subject cannot be null");
Assert.notNull(
assertion.getSubject().getNameID(), "assertion subject NameID cannot be null");
Assert.notNull(attributes, "attributes cannot be null");
Assert.notNull(attributeMappings, "attributeMappings cannot be null");
final Name name =
new Name()
.setFamilyName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getFamilyName))
.setGivenName(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getGivenName));
final List<Email> emails = new ArrayList<>(1);
emails.add(
new Email()
.setValue(
getAttribute(
attributes,
attributeMappings,
SimpleScimMappings::getEmail))
.setPrimary(true));
userResource =
new UserResource()
.setUserName(assertion.getSubject().getNameID().getValue())
.setName(name)
.setEmails(emails);
}
private static String getAttribute(
final Map<String, List<Object>> attributes,
final SimpleScimMappings simpleScimMappings,
final Function<SimpleScimMappings, String> attributeMapper) {
final String key = attributeMapper.apply(simpleScimMappings);
final List<Object> values = attributes.getOrDefault(key, Collections.emptyList());
return values.stream()
.filter(String.class::isInstance)
.map(String.class::cast)
.findFirst()
.orElse(null);
}
@Override
public String getName() {
return this.userResource.getUserName();
}
public UserResource getUserResource() {
return this.userResource;
}
}

View File

@@ -0,0 +1,10 @@
package stirling.software.SPDF.config.security.saml;
import lombok.Data;
@Data
public class SimpleScimMappings {
String givenName;
String familyName;
String email;
}

View File

@@ -1,48 +0,0 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream;
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 java.util.Base64;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
String certificateString =
new String(
FileCopyUtils.copyToByteArray(certificateResource.getInputStream()),
StandardCharsets.UTF_8);
String certContent =
certificateString
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
CertificateFactory cf = CertificateFactory.getInstance("X.509");
byte[] decodedCert = Base64.getDecoder().decode(certContent);
return (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(decodedCert));
}
public static RSAPrivateKey readPrivateKey(Resource privateKeyResource) throws Exception {
String privateKeyString =
new String(
FileCopyUtils.copyToByteArray(privateKeyResource.getInputStream()),
StandardCharsets.UTF_8);
String privateKeyContent =
privateKeyString
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\R", "")
.replaceAll("\\s+", "");
KeyFactory kf = KeyFactory.getInstance("RSA");
byte[] decodedKey = Base64.getDecoder().decode(privateKeyContent);
return (RSAPrivateKey) kf.generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
}
}

View File

@@ -1,45 +0,0 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
public class CustomSaml2AuthenticatedPrincipal
implements Saml2AuthenticatedPrincipal, Serializable {
private final String name;
private final Map<String, List<Object>> attributes;
private final String nameId;
private final List<String> sessionIndexes;
public CustomSaml2AuthenticatedPrincipal(
String name,
Map<String, List<Object>> attributes,
String nameId,
List<String> sessionIndexes) {
this.name = name;
this.attributes = attributes;
this.nameId = nameId;
this.sessionIndexes = sessionIndexes;
}
@Override
public String getName() {
return this.name;
}
@Override
public Map<String, List<Object>> getAttributes() {
return this.attributes;
}
public String getNameId() {
return this.nameId;
}
public List<String> getSessionIndexes() {
return this.sessionIndexes;
}
}

View File

@@ -1,38 +0,0 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.ProviderNotFoundException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2Error;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error();
getRedirectStrategy()
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) {
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
}
log.error("AuthenticationException: " + exception);
}
}

View File

@@ -1,91 +0,0 @@
package stirling.software.SPDF.config.security.saml2;
import java.io.IOException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.AllArgsConstructor;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
@AllArgsConstructor
public class CustomSaml2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private UserService userService;
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
Object principal = authentication.getPrincipal();
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
String username = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
// Get the saved request
HttpSession session = request.getSession(false);
String contextPath = request.getContextPath();
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(
contextPath, savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
SAML2 saml2 = applicationProperties.getSecurity().getSaml2();
if (loginAttemptService.isBlocked(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()) {
response.sendRedirect(
contextPath + "/logout?oauth2AuthenticationErrorWeb=true");
return;
}
try {
if (saml2.getBlockRegistration()
&& !userService.usernameExistsIgnoreCase(username)) {
response.sendRedirect(
contextPath + "/login?erroroauth=oauth2_admin_blocked_user");
return;
}
userService.processOAuth2PostLogin(username, saml2.getAutoCreateUser());
response.sendRedirect(contextPath + "/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect(contextPath + "/logout?invalidUsername=true");
return;
}
}
} else {
super.onAuthenticationSuccess(request, response, authentication);
}
}
}

View File

@@ -1,86 +0,0 @@
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;
import org.opensaml.saml.saml2.core.AuthnStatement;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider.ResponseToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.User;
@Component
@Slf4j
public class CustomSaml2ResponseAuthenticationConverter
implements Converter<ResponseToken, Saml2Authentication> {
private UserService userService;
public CustomSaml2ResponseAuthenticationConverter(UserService userService) {
this.userService = userService;
}
@Override
public Saml2Authentication convert(ResponseToken responseToken) {
// Extract the assertion from the response
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
// Extract the NameID
String nameId = assertion.getSubject().getNameID().getValue();
Optional<User> userOpt = userService.findByUsernameIgnoreCase(nameId);
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_USER");
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
simpleGrantedAuthority =
new SimpleGrantedAuthority(userService.findRole(user).getAuthority());
}
}
// 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);
// 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;
}
}

View File

@@ -16,7 +16,6 @@ import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;
import jakarta.transaction.Transactional;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.model.SessionEntity;
@Component
@@ -51,8 +50,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}
@@ -82,8 +79,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
principalName = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
principalName = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
principalName = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
principalName = (String) principal;
}

View File

@@ -9,7 +9,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
@@ -22,7 +21,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
*/
@RestController
@RequestMapping("/api/v1/general")
@Tag(name = "General", description = "General APIs")
public class PdfImageRemovalController {
// Service for removing images from PDFs

View File

@@ -109,7 +109,7 @@ public class ScalePagesController {
}
private PDRectangle getTargetSize(String targetPDRectangle, PDDocument sourceDocument) {
if ("KEEP".equals(targetPDRectangle)) {
if (targetPDRectangle.equals("KEEP")) {
if (sourceDocument.getNumberOfPages() == 0) {
return null;
}

View File

@@ -32,7 +32,6 @@ public class SettingsController {
}
GeneralUtils.saveKeyToConfig("system.enableAnalytics", String.valueOf(enabled), false);
applicationProperties.getSystem().setEnableAnalytics(String.valueOf(enabled));
return ResponseEntity.ok("Updated");
}
}

View File

@@ -32,7 +32,6 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
@@ -337,8 +336,6 @@ public class UserController {
userNameP = ((UserDetails) principal).getUsername();
} else if (principal instanceof OAuth2User) {
userNameP = ((OAuth2User) principal).getName();
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
userNameP = ((CustomSaml2AuthenticatedPrincipal) principal).getName();
} else if (principal instanceof String) {
userNameP = (String) principal;
}

View File

@@ -82,7 +82,7 @@ public class ConvertImgPDFController {
result =
PdfUtils.convertFromPdf(
pdfBytes,
"webp".equalsIgnoreCase(imageFormat) ? "png" : imageFormat.toUpperCase(),
imageFormat.equalsIgnoreCase("webp") ? "png" : imageFormat.toUpperCase(),
colorTypeResult,
singleImage,
Integer.valueOf(dpi),
@@ -90,9 +90,9 @@ public class ConvertImgPDFController {
if (result == null || result.length == 0) {
logger.error("resultant bytes for {} is null, error converting ", filename);
}
if ("webp".equalsIgnoreCase(imageFormat) && !CheckProgramInstall.isPythonAvailable()) {
if (imageFormat.equalsIgnoreCase("webp") && !CheckProgramInstall.isPythonAvailable()) {
throw new IOException("Python is not installed. Required for WebP conversion.");
} else if ("webp".equalsIgnoreCase(imageFormat)
} else if (imageFormat.equalsIgnoreCase("webp")
&& CheckProgramInstall.isPythonAvailable()) {
// Write the output stream to a temp file
Path tempFile = Files.createTempFile("temp_png", ".png");

View File

@@ -4,6 +4,4 @@ public interface UserServiceInterface {
String getApiKeyForUser(String username);
String getCurrentUsername();
long getTotalUsersCount();
}

View File

@@ -21,13 +21,10 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.*;
import stirling.software.SPDF.model.ApplicationProperties.Security;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2.Client;
import stirling.software.SPDF.model.ApplicationProperties.Security.SAML2;
import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider;
@@ -54,44 +51,27 @@ public class AccountWebController {
Map<String, String> providerList = new HashMap<>();
Security securityProps = applicationProperties.getSecurity();
OAUTH2 oauth = securityProps.getOauth2();
OAUTH2 oauth = applicationProperties.getSecurity().getOauth2();
if (oauth != null) {
if (oauth.getEnabled()) {
if (oauth.isSettingsValid()) {
providerList.put("/oauth2/authorization/oidc", oauth.getProvider());
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + google.getName(),
google.getClientName());
}
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + github.getName(),
github.getClientName());
}
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
providerList.put(
"/oauth2/authorization/" + keycloak.getName(),
keycloak.getClientName());
}
}
if (oauth.isSettingsValid()) {
providerList.put("oidc", oauth.getProvider());
}
}
Client client = oauth.getClient();
if (client != null) {
GoogleProvider google = client.getGoogle();
if (google.isSettingsValid()) {
providerList.put(google.getName(), google.getClientName());
}
SAML2 saml2 = securityProps.getSaml2();
if (saml2 != null) {
if (saml2.getEnabled()) {
providerList.put("/saml2/authenticate/" + saml2.getRegistrationId(), "SAML 2");
GithubProvider github = client.getGithub();
if (github.isSettingsValid()) {
providerList.put(github.getName(), github.getClientName());
}
KeycloakProvider keycloak = client.getKeycloak();
if (keycloak.isSettingsValid()) {
providerList.put(keycloak.getName(), keycloak.getClientName());
}
}
}
// Remove any null keys/values from the providerList
@@ -100,8 +80,9 @@ public class AccountWebController {
.removeIf(entry -> entry.getKey() == null || entry.getValue() == null);
model.addAttribute("providerlist", providerList);
model.addAttribute("loginMethod", securityProps.getLoginMethod());
model.addAttribute("altLogin", securityProps.isAltLogin());
model.addAttribute("loginMethod", applicationProperties.getSecurity().getLoginMethod());
model.addAttribute(
"oAuth2Enabled", applicationProperties.getSecurity().getOauth2().getEnabled());
model.addAttribute("currentPage", "login");
@@ -368,17 +349,6 @@ public class AccountWebController {
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
}
if (principal instanceof CustomSaml2AuthenticatedPrincipal) {
// Cast the principal object to OAuth2User
CustomSaml2AuthenticatedPrincipal userDetails =
(CustomSaml2AuthenticatedPrincipal) principal;
// Retrieve username and other attributes
username = userDetails.getName();
// Add oAuth2 Login attributes to the model
model.addAttribute("oAuth2Login", true);
}
if (username != null) {
// Fetch user details from the database
Optional<User> user =

View File

@@ -1,17 +1,13 @@
package stirling.software.SPDF.model;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@@ -22,8 +18,6 @@ import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import stirling.software.SPDF.config.YamlPropertySourceFactory;
import stirling.software.SPDF.model.provider.GithubProvider;
@@ -47,6 +41,7 @@ public class ApplicationProperties {
private AutomaticallyGenerated automaticallyGenerated = new AutomaticallyGenerated();
private EnterpriseEdition enterpriseEdition = new EnterpriseEdition();
private AutoPipeline autoPipeline = new AutoPipeline();
private static final Logger logger = LoggerFactory.getLogger(ApplicationProperties.class);
@Data
public static class AutoPipeline {
@@ -68,108 +63,41 @@ public class ApplicationProperties {
private Boolean csrfDisabled;
private InitialLogin initialLogin = new InitialLogin();
private OAUTH2 oauth2 = new OAUTH2();
private SAML2 saml2 = new SAML2();
private SAML saml = new SAML();
private int loginAttemptCount;
private long loginResetTimeMinutes;
private String loginMethod = "all";
public Boolean isAltLogin() {
return saml2.getEnabled() || oauth2.getEnabled();
}
public enum LoginMethods {
ALL("all"),
NORMAL("normal"),
OAUTH2("oauth2"),
SAML2("saml2");
private String method;
LoginMethods(String method) {
this.method = method;
}
@Override
public String toString() {
return method;
}
}
public boolean isUserPass() {
return (loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString())
|| loginMethod.equalsIgnoreCase(LoginMethods.ALL.toString()));
}
public boolean isOauth2Activ() {
return (oauth2 != null
&& oauth2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
public boolean isSaml2Activ() {
return (saml2 != null
&& saml2.getEnabled()
&& !loginMethod.equalsIgnoreCase(LoginMethods.NORMAL.toString()));
}
@Data
public static class InitialLogin {
private String username;
@ToString.Exclude private String password;
}
@Getter
@Setter
public static class SAML2 {
@Data
public static class SAML {
private Boolean enabled = false;
private Boolean autoCreateUser = false;
private Boolean blockRegistration = false;
private String registrationId = "stirling";
private String idpMetadataUri;
private String idpSingleLogoutUrl;
private String idpSingleLoginUrl;
private String idpIssuer;
private String idpCert;
private String privateKey;
private String spCert;
private String entityId;
private String registrationId;
private String spBaseUrl;
private String idpMetadataLocation;
private KeyStore keystore;
public InputStream getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) {
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
.getInputStream();
}
try {
URI uri = new URI(idpMetadataUri);
URL url = uri.toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getInputStream();
} catch (URISyntaxException e) {
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
}
}
@Data
public static class KeyStore {
private String keystoreLocation;
private String keystorePassword;
private String keyAlias;
private String keyPassword;
private String realmCertificateAlias;
public Resource getSpCert() {
if (spCert.startsWith("classpath:")) {
return new ClassPathResource(spCert.substring("classpath:".length()));
} else {
return new FileSystemResource(spCert);
}
}
public Resource getidpCert() {
if (idpCert.startsWith("classpath:")) {
return new ClassPathResource(idpCert.substring("classpath:".length()));
} else {
return new FileSystemResource(idpCert);
}
}
public Resource getPrivateKey() {
if (privateKey.startsWith("classpath:")) {
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
return new FileSystemResource(privateKey);
public Resource getKeystoreResource() {
if (keystoreLocation.startsWith("classpath:")) {
return new ClassPathResource(
keystoreLocation.substring("classpath:".length()));
} else {
return new FileSystemResource(keystoreLocation);
}
}
}
}

View File

@@ -19,6 +19,7 @@ public class Provider implements ProviderInterface {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
protected boolean isValid(Collection<String> value, String name) {
@@ -26,55 +27,66 @@ public class Provider implements ProviderInterface {
return true;
}
return false;
// throw new IllegalArgumentException(getName() + ": " + name + " is required!");
}
@Override
public Collection<String> getScopes() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getScope'");
}
@Override
public void setScopes(String scopes) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setScope'");
}
@Override
public String getUseAsUsername() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getUseAsUsername'");
}
@Override
public void setUseAsUsername(String useAsUsername) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setUseAsUsername'");
}
@Override
public String getIssuer() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getIssuer'");
}
@Override
public void setIssuer(String issuer) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setIssuer'");
}
@Override
public String getClientSecret() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientSecret'");
}
@Override
public void setClientSecret(String clientSecret) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientSecret'");
}
@Override
public String getClientId() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getClientId'");
}
@Override
public void setClientId(String clientId) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'setClientId'");
}
}

View File

@@ -19,7 +19,6 @@ import org.springframework.stereotype.Service;
import com.posthog.java.PostHog;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
@@ -27,18 +26,15 @@ public class PostHogService {
private final PostHog postHog;
private final String uniqueId;
private final ApplicationProperties applicationProperties;
private final UserServiceInterface userService;
@Autowired
public PostHogService(
PostHog postHog,
@Qualifier("UUID") String uuid,
ApplicationProperties applicationProperties, @Autowired(required = false) UserServiceInterface userService) {
ApplicationProperties applicationProperties) {
this.postHog = postHog;
this.uniqueId = uuid;
this.applicationProperties = applicationProperties;
this.userService = userService;
captureSystemInfo();
}
@@ -137,11 +133,6 @@ public class PostHogService {
metrics.put("docker_metrics", getDockerMetrics());
}
metrics.put("application_properties", captureApplicationProperties());
if(userService != null) {
metrics.put("total_users_created", userService.getTotalUsersCount());
}
} catch (Exception e) {
metrics.put("error", e.getMessage());

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=تم تعطيل المستخدم، تم حظر تسجيل
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=حجب تلقائي

View File

@@ -3,8 +3,8 @@
###########
# the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr
addPageNumbers.fontSize=Размер на шрифт
addPageNumbers.fontName=Име на шрифт
addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name
pdfPrompt=Изберете PDF(и)
multiPdfPrompt=Изберете PDF (2+)
multiPdfDropPrompt=Изберете (или плъзнете и пуснете) всички PDF файлове, от които се нуждаете
@@ -56,12 +56,12 @@ userNotFoundMessage=Потребителят не е намерен
incorrectPasswordMessage=Текущата парола е неправилна.
usernameExistsMessage=Новият потребител вече съществува.
invalidUsernameMessage=Невалидно потребителско име, потребителското име може да съдържа само букви, цифри и следните специални знаци @._+- или трябва да е валиден имейл адрес.
invalidPasswordMessage=Паролата не трябва да е празна и не трябва да има интервали в началото или в края.
confirmPasswordErrorMessage=Нова парола и Потвърждаване на новата парола трябва да съвпадат.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=New Password and Confirm New Password must match.
deleteCurrentUserMessage=Не може да се изтрие вписания в момента потребител.
deleteUsernameExistsMessage=Потребителското име не съществува и не може да бъде изтрито.
downgradeCurrentUserMessage=Не може да се понижи ролята на текущия потребител
disabledCurrentUserMessage=Текущият потребител не може да бъде деактивиран
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Не може да се понижи ролята на текущия потребител. Следователно текущият потребител няма да бъде показан.
userAlreadyExistsOAuthMessage=Потребителят вече съществува като OAuth2 потребител.
userAlreadyExistsWebMessage=Потребителят вече съществува като уеб-потребител.
@@ -75,16 +75,16 @@ visitGithub=Посетете Github Repository
donate=Направете дарение
color=Цвят
sponsor=Спонсор
info=Информация
info=Info
pro=Pro
page=Страница
pages=Страници
page=Page
pages=Pages
legal.privacy=Политика за поверителност
legal.terms=Правила и условия
legal.accessibility=Достъпност
legal.cookie=Политика за бисквитки
legal.impressum=Отпечатък
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
legal.accessibility=Accessibility
legal.cookie=Cookie Policy
legal.impressum=Impressum
###############
# Pipeline #
@@ -96,7 +96,7 @@ pipeline.defaultOption=Персонализиран
pipeline.submitButton=Подайте
pipeline.help=Pipeline Помощ
pipeline.scanHelp=Помощ за сканиране на папки
pipeline.deletePrompt=Сигурни ли сте, че искате да изтриете pipeline
pipeline.deletePrompt=Are you sure you want to delete pipeline
######################
# Pipeline Options #
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Валидирай
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Направете надстройка до Pro версията
enterpriseEdition.warning=Тази функция е достъпна само за потребители на Pro версията.
enterpriseEdition.yamlAdvert=Stirling PDF Pro поддържа YAML конфигурационни файлове и други SSO функции.
enterpriseEdition.ssoAdvert=Търсите повече функции за управление на потребителите? Погледнете за Stirling PDF Pro
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Искате ли да подобрите Stirling PDF?
analytics.paragraph1=Stirling PDF включва анализи, за да ни помогне да подобрим продукта. Ние не проследяваме лична информация или съдържание на файлове.
analytics.paragraph2=Моля, обмислете възможността за анализ, за ​​да помогнете на Stirling-PDF да расте и да ни позволи да разберем по-добре нашите потребители.
analytics.enable=Активиране на анализа
analytics.disable=Деактивиране на анализа
analytics.settings=Можете да промените настройките за анализ във config/settings.yml файла
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@@ -145,7 +145,7 @@ navbar.sections.convertFrom=Преобразуване от PDF
navbar.sections.security=Подписване и сигурност
navbar.sections.advance=Разширено
navbar.sections.edit=Преглед и редактиране
navbar.sections.popular=Популярни
navbar.sections.popular=Popular
#############
# SETTINGS #
@@ -202,9 +202,9 @@ adminUserSettings.header=Настройки за администраторск
adminUserSettings.admin=Администратор
adminUserSettings.user=Потребител
adminUserSettings.addUser=Добавяне на нов потребител
adminUserSettings.deleteUser=Изтриване на потребител
adminUserSettings.confirmDeleteUser=Трябва ли потребителят да бъде изтрит?
adminUserSettings.confirmChangeUserStatus=Трябва ли потребителят да бъде деактивиран/активиран?
adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Потребителското име може да съдържа само букви, цифри и следните специални символи @._+- или трябва да е валиден имейл адрес.
adminUserSettings.roles=Роли
adminUserSettings.role=Роля
@@ -218,32 +218,32 @@ adminUserSettings.forceChange=Принудете потребителя да п
adminUserSettings.submit=Съхранете потребителя
adminUserSettings.changeUserRole=Промяна на ролята на потребителя
adminUserSettings.authenticated=Удостоверен
adminUserSettings.editOwnProfil=Редактиране на собствен профил
adminUserSettings.enabledUser=активиран потребител
adminUserSettings.disabledUser=деактивиран потребител
adminUserSettings.activeUsers=Активни потребители:
adminUserSettings.disabledUsers=Деактивирани потребители:
adminUserSettings.totalUsers=Общо потребители:
adminUserSettings.lastRequest=Последна заявка
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Импорт/Експорт на база данни
database.header=Импорт/Експорт на база данни
database.fileName=Име на файл
database.creationDate=Дата на създаване
database.fileSize=Размер на файла
database.deleteBackupFile=Изтриване на архивен файл
database.importBackupFile=Импортиране на архивен файл
database.downloadBackupFile=Изтеглете архивен файл
database.info_1=Когато импортирате данни, е от решаващо значение да осигурите правилната структура. Ако не сте сигурни в това, което правите, потърсете съвет и подкрепа от професионалист. Грешка в структурата може да причини неизправност на приложението, включително пълна невъзможност за стартиране на приложението.
database.info_2=Името на файла няма значение при качване. След това ще бъде преименуван, за да следва формата backup_user_yyyyMMddHHmm.sql, осигурявайки последователна конвенция за именуване.
database.submit=Импортиране на резервно копие
database.importIntoDatabaseSuccessed=Импортирането в базата данни бе успешно
database.fileNotFound=Файлът не е намерен
database.fileNullOrEmpty=Файлът не трябва да е нулев или празен
database.failedImportFile=Неуспешно импортиране на файл
database.title=Database Import/Export
database.header=Database Import/Export
database.fileName=File Name
database.creationDate=Creation Date
database.fileSize=File Size
database.deleteBackupFile=Delete Backup File
database.importBackupFile=Import Backup File
database.downloadBackupFile=Download Backup File
database.info_1=When importing data, it is crucial to ensure the correct structure. If you are unsure of what you are doing, seek advice and support from a professional. An error in the structure can cause application malfunctions, up to and including the complete inability to run the application.
database.info_2=The file name does not matter when uploading. It will be renamed afterward to follow the format backup_user_yyyyMMddHHmm.sql, ensuring a consistent naming convention.
database.submit=Import Backup
database.importIntoDatabaseSuccessed=Import into database successed
database.fileNotFound=File not Found
database.fileNullOrEmpty=File must not be null or empty
database.failedImportFile=Failed Import File
session.expired=Вашата сесия е изтекла. Моля, опреснете страницата и опитайте отново.
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
@@ -390,9 +390,9 @@ home.certSign.title=Подпишете със сертификат
home.certSign.desc=Подписва PDF със сертификат/ключ (PEM/P12)
certSign.tags=удостоверяване,PEM,P12,официален,шифроване
home.removeCertSign.title=Премахване на знака за сертификат
home.removeCertSign.desc=Премахване на подпис на сертификат от PDF
removeCertSign.tags=удостоверяване,PEM,P12,официален,декриптиране
home.removeCertSign.title=Remove Certificate Sign
home.removeCertSign.desc=Remove certificate signature from PDF
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=Оформление с няколко страници
home.pageLayout.desc=Слейте няколко страници от PDF документ в една страница
@@ -498,33 +498,33 @@ home.BookToPDF.title=Книга към PDF
home.BookToPDF.desc=Преобразува формати на книги/комикси в PDF с помощта на calibre
BookToPDF.tags=Книга,комикс,calibre,конвертиране,манга,Amazon,Kindle
home.removeImagePdf.title=Премахване на изображение
home.removeImagePdf.desc=Премахнете изображението от PDF, за да намалите размера на файла
removeImagePdf.tags=Премахване на изображение, операции на страници, админ страна, страна на сървъра
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=Разделете PDF по глави
home.splitPdfByChapters.desc=Разделете PDF на множество файлове въз основа на неговата структура на глави.
splitPdfByChapters.tags=разделяне, глави, отметки, организиране
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=Замени-инвертиране-на-цвят
replace-color.header=Замяна-инвертиране на цвят PDF
home.replaceColorPdf.title=Замяна и обръщане на цвят
home.replaceColorPdf.desc=Заменете цвета на текста и фона в PDF и обърнете пълния цвят на PDF, за да намалите размера на файла
replaceColorPdf.tags=Замяна на цвят, операции на страници, заден край, страна на сървъра
replace-color.selectText.1=Опции за замяна или инвертиране на цвят
replace-color.selectText.2=По подразбиране (цветове с висок контраст по подразбиране)
replace-color.selectText.3=По избор (персонализирани цветове)
replace-color.selectText.4=Пълно инвертиране (Инвертиране на всички цветове)
replace-color.selectText.5=Цветови опции с висок контраст
replace-color.selectText.6=Бял текст на черен фон
replace-color.selectText.7=Черен текст на бял фон
replace-color.selectText.8=Жълт текст на черен фон
replace-color.selectText.9=Зелен текст на черен фон
replace-color.selectText.10=Изберете цвят на текста
replace-color.selectText.11=Изберете цвят на фона
replace-color.submit=Замени
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
@@ -543,17 +543,18 @@ login.locked=Вашият акаунт е заключен.
login.signinTitle=Моля впишете се
login.ssoSignIn=Влизане чрез еднократно влизане
login.oauth2AutoCreateDisabled=OAUTH2 Автоматично създаване на потребител е деактивирано
login.oauth2AdminBlockedUser=Регистрацията или влизането на нерегистрирани потребители в момента е блокирано. Моля, свържете се с администратора.
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Заявката за оторизация не е намерена
login.oauth2InvalidUserInfoResponse=Невалидна информация за потребителя
login.oauth2invalidRequest=Невалидна заявка
login.oauth2AccessDenied=Отказан достъп
login.oauth2InvalidTokenResponse=Невалиден отговор на токена
login.oauth2InvalidIdToken=Невалиден токен за идентификатор
login.userIsDisabled=Потребителят е деактивиран, влизането в момента е блокирано с това потребителско име. Моля, свържете се с администратора.
login.alreadyLoggedIn=Вече сте влезли в
login.alreadyLoggedIn2=устройства. Моля, излезте от устройствата и опитайте отново.
login.toManySessions=Имате твърде много активни сесии
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Автоматично редактиране
@@ -728,7 +729,7 @@ pageLayout.submit=Подайте
scalePages.title=Коригиране на мащаба на страницата
scalePages.header=Коригиране на мащаба на страницата
scalePages.pageSize=Размер на страница от документа.
scalePages.keepPageSize=Оригинален размер
scalePages.keepPageSize=Original Size
scalePages.scaleFactor=Ниво на мащабиране (изрязване) на страница.
scalePages.submit=Подайте
@@ -752,10 +753,10 @@ certSign.submit=Подпишете PDF
#removeCertSign
removeCertSign.title=Премахване на подписа на сертификата
removeCertSign.header=Премахнете цифровия сертификат от PDF
removeCertSign.selectPDF=Изберете PDF файл:
removeCertSign.submit=Премахване на подпис
removeCertSign.title=Remove Certificate Signature
removeCertSign.header=Remove the digital certificate from the PDF
removeCertSign.selectPDF=Select a PDF file:
removeCertSign.submit=Remove Signature
#removeBlanks
@@ -777,8 +778,8 @@ removeAnnotations.submit=Премахване
#compare
compare.title=Сравнявай
compare.header=Сравнявай PDF-и
compare.highlightColor.1=Цвят на маркирането 1:
compare.highlightColor.2=Цвят на маркирането 2:
compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Color 2:
compare.document.1=Документ 1
compare.document.2=Документ 2
compare.submit=Сравнявай
@@ -830,7 +831,7 @@ ScannerImageSplit.selectText.7=Минимална контурна площ:
ScannerImageSplit.selectText.8=Задава минималния праг на контурната площ за изображение
ScannerImageSplit.selectText.9=Размер на рамката:
ScannerImageSplit.selectText.10=Задава размера на добавената и премахната граница, за да предотврати бели граници към изхода (по подразбиране: 1).
ScannerImageSplit.info=Python не е инсталиран. Изисква се да се изпълнява.
ScannerImageSplit.info=Python is not installed. It is required to run.
#OCR
@@ -857,7 +858,7 @@ ocr.submit=Обработка на PDF чрез OCR
extractImages.title=Извличане на изображения
extractImages.header=Извличане на изображения
extractImages.selectText=Изберете формат на изображението, в който да преобразувате извлечените изображения
extractImages.allowDuplicates=Запазване на дублирани изображения
extractImages.allowDuplicates=Save duplicate images
extractImages.submit=Извличане
@@ -895,7 +896,7 @@ merge.title=Обединяване
merge.header=Обединяване на множество PDF файлове (2+)
merge.sortByName=Сортиране по име
merge.sortByDate=Сортиране по дата
merge.removeCertSign=Премахване на цифровия подпис в обединения файл?
merge.removeCertSign=Remove digital signature in the merged file?
merge.submit=Обединяване
@@ -913,7 +914,7 @@ pdfOrganiser.mode.6=Четно-нечетно разделяне
pdfOrganiser.mode.7=Премахни първо
pdfOrganiser.mode.8=Премахване на последния
pdfOrganiser.mode.9=Премахване на първия и последния
pdfOrganiser.mode.10=Обединяване на четно и нечетно
pdfOrganiser.mode.10=Odd-Even Merge
pdfOrganiser.placeholder=(напр. 1,3,2 или 4-8,2,10-12 или 2n-1)
@@ -982,7 +983,7 @@ pdfToImage.color=Цвят
pdfToImage.grey=Скала на сивото
pdfToImage.blackwhite=Черно и бяло (може да загубите данни!)
pdfToImage.submit=Преобразуване
pdfToImage.info=Python не е инсталиран. Изисква се за конвертиране на WebP.
pdfToImage.info=Python is not installed. Required for WebP conversion.
#addPassword
@@ -1019,7 +1020,7 @@ watermark.selectText.6=дължинаSpacer (Разстояние между в
watermark.selectText.7=Непрозрачност (0% - 100%):
watermark.selectText.8=Тип воден знак:
watermark.selectText.9=Изображение за воден знак:
watermark.selectText.10=Конвертирайте PDF в PDF-изображение
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Добавяне на воден знак
watermark.type.1=Текст
watermark.type.2=Изображение
@@ -1076,7 +1077,7 @@ pdfToPDFA.credit=Тази услуга използва ghostscript за PDF/A
pdfToPDFA.submit=Преобразуване
pdfToPDFA.tip=В момента не работи за няколко входа наведнъж
pdfToPDFA.outputFormat=Изходен формат
pdfToPDFA.pdfWithDigitalSignature=PDF файлът съдържа цифров подпис. Това ще бъде премахнато в следващата стъпка.
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
#PDFToWord
@@ -1117,10 +1118,10 @@ PDFToXML.credit=Тази услуга използва LibreOffice за прео
PDFToXML.submit=Преобразуване
#PDFToCSV
PDFToCSV.title=PDF към CSV
PDFToCSV.header=PDF към CSV
PDFToCSV.title=PDF ??? CSV
PDFToCSV.header=PDF ??? CSV
PDFToCSV.prompt=Изберете страница за извличане на таблица
PDFToCSV.submit=Преобразуване
PDFToCSV.submit=????
#split-by-size-or-count
split-by-size-or-count.title=Разделяне на PDF по размер или брой
@@ -1178,15 +1179,15 @@ licenses.version=Версия
licenses.license=Лиценз
#survey
survey.nav=Анкета
survey.title=Stirling-PDF Анкета
survey.description=Stirling-PDF няма проследяване, така че искаме да чуем мнението на нашите потребители за подобряване на Stirling-PDF!
survey.changes=Stirling-PDF се промени от последното проучване! За да научите повече, моля, проверете публикацията в нашия блог тук:
survey.changes2=С тези промени получаваме платена бизнес подкрепа и финансиране
survey.please=Моля, помислете дали да не участвате в нашата анкета!
survey.disabled=(Изскачащият прозорец с анкетата ще бъде деактивиран при следващите актуализации, но ще бъде наличен в долната част на страницата)
survey.button=Участвайте в анкетата
survey.dontShowAgain=Не показвай повече
survey.nav=Survey
survey.title=Stirling-PDF Survey
survey.description=Stirling-PDF has no tracking so we want to hear from our users to improve Stirling-PDF!
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.please=Please consider taking our survey!
survey.disabled=(Survey popup will be disabled in following updates but available at foot of page)
survey.button=Take Survey
survey.dontShowAgain=Don't show again
#error
@@ -1204,21 +1205,21 @@ error.discordSubmit=Discord - Изпратете запитване за под
#remove-image
removeImage.title=Премахване на изображението
removeImage.header=Премахване на изображението
removeImage.removeImage=Премахване на изображението
removeImage.submit=Премахване на изображението
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image
splitByChapters.title=Разделете PDF по глави
splitByChapters.header=Разделете PDF по глави
splitByChapters.bookmarkLevel=Ниво на отметка
splitByChapters.includeMetadata=Включете метаданни
splitByChapters.allowDuplicates=Разрешаване на дубликати
splitByChapters.desc.1=Този инструмент разделя PDF файл на множество PDF файлове въз основа на неговата структура на глави.
splitByChapters.desc.2=Ниво на отметка: Изберете нивото на отметките, които да използвате за разделяне (0 за най-високо ниво, 1 за второ ниво и т.н.).
splitByChapters.desc.3=Включване на метаданни: Ако е отметнато, метаданните на оригиналния PDF ще бъдат включени във всеки разделен PDF.
splitByChapters.desc.4=Разрешаване на дубликати: Ако е отметнато, позволява множество отметки на една и съща страница за създаване на отделни PDF файлове.
splitByChapters.submit=Разделяне на PDF
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=Bruger er deaktiveret, login er i øjeblikket blokeret med
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Rediger

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=Benutzer ist deaktiviert, die Anmeldung ist mit diesem Benu
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatisch zensieren/schwärzen

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Αυτόματο Μαύρισμα Κειμένου

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=El usuario está desactivado, actualmente el acceso está b
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redactar

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Idatzi

View File

@@ -145,7 +145,7 @@ navbar.sections.convertFrom=Convertir depuis PDF
navbar.sections.security=Signature et sécurité
navbar.sections.advance=Mode avancé
navbar.sections.edit=Voir et modifier
navbar.sections.popular=Populaire
navbar.sections.popular=Popular
#############
# SETTINGS #
@@ -202,9 +202,9 @@ adminUserSettings.header=Administration des paramètres des utilisateurs
adminUserSettings.admin=Administateur
adminUserSettings.user=Utilisateur
adminUserSettings.addUser=Ajouter un utilisateur
adminUserSettings.deleteUser=Supprimer l'utilisateur
adminUserSettings.confirmDeleteUser=Voulez vous vraiment supprimer l'utilisateur ?
adminUserSettings.confirmChangeUserStatus=Voulez vous vraiment déactiver/réactiver l'utilisateur ?
adminUserSettings.deleteUser=Delete User
adminUserSettings.confirmDeleteUser=Should the user be deleted?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et les caractères spéciaux suivants @._+- ou doit être une adresse e-mail valide.
adminUserSettings.roles=Rôles
adminUserSettings.role=Rôle
@@ -218,13 +218,13 @@ adminUserSettings.forceChange=Forcer lutilisateur à changer son nom dutil
adminUserSettings.submit=Ajouter
adminUserSettings.changeUserRole=Changer le rôle de l'utilisateur
adminUserSettings.authenticated=Authentifié
adminUserSettings.editOwnProfil=Éditer son propre profil
adminUserSettings.enabledUser=Utilisateur activé
adminUserSettings.disabledUser=Utilisateur désactivé
adminUserSettings.activeUsers=Utilisateurs actifs :
adminUserSettings.disabledUsers=Utilisateurs désactivés :
adminUserSettings.totalUsers=Utilisateurs au total :
adminUserSettings.lastRequest=Dernière requête
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Database Import/Export
@@ -243,7 +243,7 @@ database.fileNotFound=File not Found
database.fileNullOrEmpty=File must not be null or empty
database.failedImportFile=Failed Import File
session.expired=Votre session a expiré. Veuillez recharger la page et réessayer.
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
@@ -321,7 +321,7 @@ home.changeMetadata.desc=Modifiez, supprimez ou ajoutez des métadonnées à un
changeMetadata.tags=métadonnées,titre,auteur,date,création,heure,éditeur,statistiques,title,author,date,creation,time,publisher,producer,stats,metadata
home.fileToPDF.title=Fichier en PDF
home.fileToPDF.desc=Convertissez presque nimporte quel fichier en PDF (DOCX, PNG, XLS, PPT, TXT, etc.).
home.fileToPDF.desc=Convertissez presque nimporte quel fichiers en PDF (DOCX, PNG, XLS, PPT, TXT et plus).
fileToPDF.tags=convertion,transformation,format,document,image,slide,texte,conversion,office,docs,word,excel,powerpoint
home.ocr.title=OCR / Nettoyage des numérisations
@@ -390,9 +390,9 @@ home.certSign.title=Signer avec un certificat
home.certSign.desc=Signez un PDF avec un certificat ou une clé (PEM/P12).
certSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,encrypt
home.removeCertSign.title=Supprimer la signature par certificat
home.removeCertSign.desc=Supprimez la signature par certificat d'un PDF
removeCertSign.tags=signer,chiffrer,certificat,authenticate,PEM,P12,official,decrypt
home.removeCertSign.title=Remove Certificate Sign
home.removeCertSign.desc=Remove certificate signature from PDF
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=Fusionner des pages
home.pageLayout.desc=Fusionnez plusieurs pages dun PDF en une seule.
@@ -498,14 +498,14 @@ home.BookToPDF.title=eBook vers PDF
home.BookToPDF.desc=Convertit les formats de livres/bandes dessinées en PDF à l'aide de calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Supprimer les images
home.removeImagePdf.desc=Supprimez les images d'un PDF pour réduire sa taille
removeImagePdf.tags=Images,Remove Image,Page operations,Back end,server side
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=Séparer un PDF par chapitres
home.splitPdfByChapters.desc=Séparez un PDF en fichiers multiples en fonction de sa structure par chapitres.
splitPdfByChapters.tags=séparer,chapitres,split,chapters,bookmarks,organize
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=Replace-Invert-Color
@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Caviarder automatiquement
@@ -786,14 +787,14 @@ compare.submit=Comparer
#BookToPDF
BookToPDF.title=Livres et BD vers PDF
BookToPDF.header=Livre vers PDF
BookToPDF.credit=Utilise Calibre
BookToPDF.credit=Utiliser Calibre
BookToPDF.submit=Convertir
#PDFToBook
PDFToBook.title=PDF vers Livre
PDFToBook.header=PDF vers Livre
PDFToBook.selectText.1=Format
PDFToBook.credit=Utilise Calibre
PDFToBook.credit=Utiliser Calibre
PDFToBook.submit=Convertir
#sign
@@ -1076,7 +1077,7 @@ pdfToPDFA.credit=Ce service utilise ghostscript pour la conversion en PDF/A.
pdfToPDFA.submit=Convertir
pdfToPDFA.tip=Ne fonctionne actuellement pas pour plusieurs entrées à la fois
pdfToPDFA.outputFormat=Format de sortie
pdfToPDFA.pdfWithDigitalSignature=Le PDF contient une signature numérique. Elle sera supprimée dans l'étape suivante.
pdfToPDFA.pdfWithDigitalSignature=The PDF contains a digital signature. This will be removed in the next step.
#PDFToWord

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Redact

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=स्वत: गोपनीयकरण

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatsko uređivanje

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Érzékeny tartalom eltávolítása

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Redaksional Otomatis

View File

@@ -77,8 +77,8 @@ color=Colore
sponsor=Sponsor
info=Info
pro=Pro
page=Pagina
pages=Pagine
page=Page
pages=Pages
legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Convalidare
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Aggiorna alla versione Pro
enterpriseEdition.warning=Questa funzionalità è disponibile solo per gli utenti Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supporta i file di configurazione YAML e altre funzionalità SSO.
enterpriseEdition.ssoAdvert=Cerchi altre funzionalità di gestione degli utenti? Dai un'occhiata a Stirling PDF Pro
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Vuoi migliorare Stirling PDF?
analytics.paragraph1=Stirling PDF ha opt-in analytics per aiutarci a migliorare il prodotto. Non tracciamo alcuna informazione personale o contenuto di file.
analytics.paragraph2=Si prega di prendere in considerazione l'attivazione dell'analytics per aiutare Stirling-PDF a crescere e consentirci di comprendere meglio i nostri utenti.
analytics.enable=Abilita analytics
analytics.disable=Disabilita analytics
analytics.settings=È possibile modificare le impostazioni per analitycs nel file config/settings.yml
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@@ -145,7 +145,7 @@ navbar.sections.convertFrom=Converti da PDF
navbar.sections.security=Firma & Sicurezza
navbar.sections.advance=Avanzate
navbar.sections.edit=Visualizza & Modifica
navbar.sections.popular=Populare
navbar.sections.popular=Popular
#############
# SETTINGS #
@@ -243,7 +243,7 @@ database.fileNotFound=File non trovato
database.fileNullOrEmpty=Il file non deve essere nullo o vuoto
database.failedImportFile=Importazione file non riuscita
session.expired=La tua sessione è scaduta. Aggiorna la pagina e riprova.
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
@@ -503,9 +503,9 @@ home.removeImagePdf.desc=Rimuovi le immagini dal PDF per ridurre la dimensione d
removeImagePdf.tags=Rimuovi immagine,operazioni sulla pagina,back-end,lato server
home.splitPdfByChapters.title=Dividi PDF per capitoli
home.splitPdfByChapters.desc=Dividi un PDF in più file in base alla struttura dei capitoli.
splitPdfByChapters.tags=dividi, capitoli, segnalibri, organizza
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=Sostituisci-Inverti-Colore
@@ -551,9 +551,10 @@ login.oauth2AccessDenied=Accesso negato
login.oauth2InvalidTokenResponse=Risposta token non valida
login.oauth2InvalidIdToken=Id Token non valido
login.userIsDisabled=L'utente è disattivato, l'accesso è attualmente bloccato con questo nome utente. Si prega di contattare l'amministratore.
login.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Redazione automatica
@@ -1181,8 +1182,8 @@ licenses.license=Licenza
survey.nav=Sondaggio
survey.title=Sondaggio Stirling-PDF
survey.description=Stirling-PDF non fa tracciamento, quindi vogliamo sentire i nostri utenti per migliorare Stirling-PDF!
survey.changes=Stirling-PDF è cambiato dall'ultimo sondaggio! Per saperne di più, consulta il nostro blog qui:
survey.changes2=Con questi cambiamenti stiamo ricevendo supporto aziendale e finanziamenti retribuiti
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.please=Ti invitiamo a prendere in considerazione la possibilità di partecipare al nostro sondaggio!
survey.disabled=(Il popup del sondaggio verrà disabilitato nei prossimi aggiornamenti ma sarà disponibile a piè di pagina)
survey.button=Partecipa al sondaggio
@@ -1210,15 +1211,15 @@ removeImage.removeImage=Rimuovi immagine
removeImage.submit=Rimuovi immagine
splitByChapters.title=Dividere PDF per capitoli
splitByChapters.header=Dividi PDF per capitoli
splitByChapters.bookmarkLevel=Livello segnalibro
splitByChapters.includeMetadata=Includi Metadati
splitByChapters.allowDuplicates=Consenti duplicati
splitByChapters.desc.1=Questo strumento divide un file PDF in più PDF in base alla struttura dei capitoli.
splitByChapters.desc.2=Livello segnalibro: seleziona il livello dei segnalibri da utilizzare per la suddivisione (0 per il livello superiore, 1 per il secondo livello, ecc.).
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
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=ユーザーは非アクティブ化されており、現
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=自動塗りつぶし

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=자동 검열

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatisch censureren

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatisk Sensurering

View File

@@ -3,8 +3,8 @@
###########
# the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr
addPageNumbers.fontSize=Rozmiar Czcionki
addPageNumbers.fontName=Nazwa Czcionki
addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name
pdfPrompt=Wybierz PDF
multiPdfPrompt=Wybierz PDF (2+)
multiPdfDropPrompt=Wybierz (lub przeciągnij i puść) wszystkie dokumenty PDF
@@ -56,12 +56,12 @@ userNotFoundMessage=Brak użytkownika.
incorrectPasswordMessage=Nieprawidłowe hasło.
usernameExistsMessage=Taki uzytkownik już istnieje.
invalidUsernameMessage=Niewłaściwa nazwa użytkownika - musi zawierać litery, cyfry i @._+- LUB być adresem email.
invalidPasswordMessage=Hasło nie może być puste i nie może zawierać spacji na początku ani na końcu.
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=Wpisz poprawnie hasło w OBA pola.
deleteCurrentUserMessage=Nie można usunąć zalogowanego użytkownika
deleteUsernameExistsMessage=Nie można usunąć zalogowanego użytkownika
downgradeCurrentUserMessage=Nie można obniżyć roli bieżącego użytkownika
disabledCurrentUserMessage=Nie można wyłączyć bieżącego użytkownika
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=Nie można obniżyć roli bieżącego użytkownika. W związku z tym bieżący użytkownik nie zostanie wyświetlony.
userAlreadyExistsOAuthMessage=Takie konto użytkownika istnieje - stworzone za pomocą OAuth2.
userAlreadyExistsWebMessage=Takie konto użytkownika istnieje - stworzone za pomocą przeglądarki.
@@ -77,14 +77,14 @@ color=kolor
sponsor=sponsor
info=informacje
pro=Pro
page=Strona
pages=Strony
page=Page
pages=Pages
legal.privacy=Polityka Prywatności
legal.terms=Zasady i Postanowienia
legal.accessibility=Dostępność
legal.cookie=Polityka plików cookie
legal.impressum=Impresja
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
legal.accessibility=Accessibility
legal.cookie=Cookie Policy
legal.impressum=Impressum
###############
# Pipeline #
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Waliduj
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Uaktualnij do wersji Pro
enterpriseEdition.warning=Ta funkcja jest dostępna tylko dla użytkowników Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro obsługuje pliki konfiguracyjne YAML i inne funkcje SSO.
enterpriseEdition.ssoAdvert=Szukasz więcej funkcji zarządzania użytkownikami? Sprawdź Stirling PDF Pro
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Czy chcesz ulepszyć Stirling PDF?
analytics.paragraph1=Stirling PDF ma opcję analizy, która pomaga nam udoskonalać produkt. Nie śledzimy żadnych danych osobowych ani zawartości plików.
analytics.paragraph2=Rozważ włączenie funkcji analitycznych, które pomogą w rozwoju Stirling-PDF i pozwolą nam lepiej zrozumieć naszych użytkowników.
analytics.enable=Włącz analitykę
analytics.disable=Wyłącz analitykę
analytics.settings=Możesz zmienić ustawienia analityki w pliku config/settings.yml
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@@ -138,14 +138,14 @@ navbar.darkmode=Tryb nocny
navbar.language=Języki
navbar.settings=Ustawienia
navbar.allTools=Narzędzia
navbar.multiTool=Narzędzie Wielofunkcyjne
navbar.multiTool=Multi Tools
navbar.sections.organize=Organizuj
navbar.sections.convertTo=Przetwórz na PDF
navbar.sections.convertFrom=Przetwórz z PDF
navbar.sections.security=Podpis i bezpieczeństwo
navbar.sections.advance=Zaawansowane
navbar.sections.edit=Podgląd i edycja
navbar.sections.popular=Popularne
navbar.sections.popular=Popular
#############
# SETTINGS #
@@ -218,13 +218,13 @@ adminUserSettings.forceChange=Wymuś zmianę hasło po zalogowaniu
adminUserSettings.submit=Zapisz użytkownika
adminUserSettings.changeUserRole=Zmień rolę użytkownika
adminUserSettings.authenticated=Zalogowany
adminUserSettings.editOwnProfil=Edytuj własny profil
adminUserSettings.enabledUser=włączony użytkownik
adminUserSettings.disabledUser=wyłączony użytkownik
adminUserSettings.activeUsers=Aktywni Użytkownicy:
adminUserSettings.disabledUsers=Wyłączeni Użytkownicy:
adminUserSettings.totalUsers=Łączna Liczba Użytkowników:
adminUserSettings.lastRequest=Ostatnie Zgłoszenie
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=Import/Eksport bazy danych
@@ -243,7 +243,7 @@ database.fileNotFound=Plik nie znaleziony
database.fileNullOrEmpty=Plik nie może być pusty
database.failedImportFile=Nie udało się zaimportować pliku
session.expired=Twoja sesja wygasła. Odśwież stronę i spróbuj ponownie.
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
@@ -256,207 +256,207 @@ home.viewPdf.title=Podejrzyj PDF
home.viewPdf.desc=Wyświetl, adnotuj, dodaj tekst lub obrazy
viewPdf.tags=wyświetl,czytaj,adnotuj,tekst,obraz
home.multiTool.title=Wielofunkcyjne Narzędzie PDF
home.multiTool.title=Multi narzędzie PDF
home.multiTool.desc=Łącz, dziel, obracaj, zmieniaj kolejność i usuwaj strony
multiTool.tags=Wielofunkcyjne narzędzie, obsługa wielu operacji, interfejs użytkownika, przeciąganie kliknięć, front-end, strona klienta
multiTool.tags=Multi Tool,Multi operation,UI,click drag,front end,client side
home.merge.title=Połącz
home.merge.desc=Łatwe łączenie wielu dokumentów PDF w jeden.
merge.tags=scalanie, operacje na stronach, back-end, po stronie serwera
merge.tags=merge,Page operations,Back end,server side
home.split.title=Podziel
home.split.desc=Podziel dokument PDF na wiele dokumentów
split.tags=Operacje na stronach, dzielenie, wiele stron, cięcie, po stronie serwera
split.tags=Page operations,divide,Multi Page,cut,server side
home.rotate.title=Obróć
home.rotate.desc=Łatwo obracaj dokumenty PDF.
rotate.tags=strona serwera
rotate.tags=server side
home.imageToPdf.title=Obraz na PDF
home.imageToPdf.desc=Konwertuj obraz (PNG, JPEG, GIF) do dokumentu PDF.
imageToPdf.tags=konwersja,img,jpg,obraz,zdjęcie
imageToPdf.tags=conversion,img,jpg,picture,photo
home.pdfToImage.title=PDF na Obraz
home.pdfToImage.desc=Konwertuj plik PDF na obraz (PNG, JPEG, GIF).
pdfToImage.tags=konwersja,img,jpg,obraz,zdjęcie
pdfToImage.tags=conversion,img,jpg,picture,photo
home.pdfOrganiser.title=Uporządkuj
home.pdfOrganiser.desc=Usuń/Zmień kolejność stron w dowolnej kolejności
pdfOrganiser.tags=duplex,parzyste,nieparzyste,sortuj,przenieś
pdfOrganiser.tags=duplex,even,odd,sort,move
home.addImage.title=Dodaj obraz
home.addImage.desc=Dodaje obraz w wybranym miejscu w dokumencie PDF
addImage.tags=img,jpg,obraz,zdjęcie
addImage.tags=img,jpg,picture,photo
home.watermark.title=Dodaj znak wodny
home.watermark.desc=Dodaj niestandardowy znak wodny do dokumentu PDF.
watermark.tags=Tekst,powtarzanie,etykieta,własne,prawa autorskie,znak wodny,img,jpg,obraz,zdjęcie
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
home.permissions.title=Zmień uprawnienia
home.permissions.desc=Zmień uprawnienia dokumentu PDF
permissions.tags=odczyt,zapis,edycja,drukowanie
permissions.tags=read,write,edit,print
home.removePages.title=Usuń
home.removePages.desc=Usuń niechciane strony z dokumentu PDF.
removePages.tags=Usuń strony,usuwaj strony
removePages.tags=Remove pages,delete pages
home.addPassword.title=Dodaj hasło
home.addPassword.desc=Zaszyfruj dokument PDF za pomocą hasła.
addPassword.tags=bezpieczeństwo,ochrona
addPassword.tags=secure,security
home.removePassword.title=Usuń hasło
home.removePassword.desc=Usuń ochronę hasłem z dokumentu PDF.
removePassword.tags=zabezpieczenie,odszyfrowanie,bezpieczeństwo,odhasłowanie,usunięcie hasła
removePassword.tags=secure,Decrypt,security,unpassword,delete password
home.compressPdfs.title=Kompresuj
home.compressPdfs.desc=Kompresuj dokumenty PDF, aby zmniejszyć ich rozmiar.
compressPdfs.tags=zgniatać,mały,malutki
compressPdfs.tags=squish,small,tiny
home.changeMetadata.title=Zmień metadane
home.changeMetadata.desc=Zmień/Usuń/Dodaj metadane w dokumencie PDF
changeMetadata.tags=Tytuł,autor,data,utworzenie,czas,wydawca,producent,statystyki
changeMetadata.tags=Title,author,date,creation,time,publisher,producer,stats
home.fileToPDF.title=Konwertuj plik do PDF
home.fileToPDF.desc=Konwertuj dowolny plik do dokumentu PDF (DOCX, PNG, XLS, PPT, TXT i więcej)
fileToPDF.tags=transformacja,format,dokument,obraz,slajd,tekst,konwersja,office,dokumenty,word,excel,powerpoint
fileToPDF.tags=transformation,format,document,picture,slide,text,conversion,office,docs,word,excel,powerpoint
home.ocr.title=OCR / Zamiana na tekst
home.ocr.desc=OCR skanuje i wykrywa tekst z obrazów w dokumencie PDF i zamienia go na tekst.
ocr.tags=rozpoznawanie, tekst, obraz, skanowanie, odczyt, identyfikacja, wykrywanie, edytowalność
ocr.tags=recognition,text,image,scan,read,identify,detection,editable
home.extractImages.title=Wyodrębnij obrazy
home.extractImages.desc=Wyodrębnia wszystkie obrazy z dokumentu PDF i zapisuje je w wybranym formacie
extractImages.tags=obraz, zdjęcie, zapisz, archiwum, zip, przechwyć, złap
extractImages.tags=picture,photo,save,archive,zip,capture,grab
home.pdfToPDFA.title=PDF na PDF/A
home.pdfToPDFA.desc=Konwertuj dokument PDF na PDF/A w celu długoterminowego przechowywania
pdfToPDFA.tags=archiwum, długoterminowe, standardowe, konwersja, przechowywanie, konserwacja
pdfToPDFA.tags=archive,long-term,standard,conversion,storage,preservation
home.PDFToWord.title=PDF na Word
home.PDFToWord.desc=Konwertuj dokument PDF na formaty Word (DOC, DOCX i ODT)
PDFToWord.tags=doc,docx,odt,word, przekształcenie, transformacja, konwersja, office, microsoft, plik doc
PDFToWord.tags=doc,docx,odt,word,transformation,format,conversion,office,microsoft,docfile
home.PDFToPresentation.title=PDF na Prezentację
home.PDFToPresentation.desc=Konwertuj dokument PDF na formaty prezentacji (PPT, PPTX i ODP)
PDFToPresentation.tags=slajdy, pokaz, office, microsoft
PDFToPresentation.tags=slides,show,office,microsoft
home.PDFToText.title=PDF na Tekst/RTF
home.PDFToText.desc=Konwertuj dokument PDF na tekst lub format RTF
PDFToText.tags=format tekstu sformatowanego,rtf format
PDFToText.tags=richformat,richtextformat,rich text format
home.PDFToHTML.title=PDF na HTML
home.PDFToHTML.desc=Konwertuj dokument PDF na format HTML
PDFToHTML.tags=zawartość internetowa, przyjazne dla przeglądarek
PDFToHTML.tags=web content,browser friendly
home.PDFToXML.title=PDF na XML
home.PDFToXML.desc=Konwertuj dokument PDF na format XML
PDFToXML.tags=ekstrakcja danych, zawartość strukturalna, współdziałanie, transformacja, konwertowanie
PDFToXML.tags=data-extraction,structured-content,interop,transformation,convert
home.ScannerImageSplit.title=Wykryj/Podziel zeskanowane zdjęcia
home.ScannerImageSplit.desc=Podziel na wiele zdjęć z jednego zdjęcia/PDF
ScannerImageSplit.tags=oddzielne, automatyczne wykrywanie, skanowanie, wiele zdjęć, porządkowanie
ScannerImageSplit.tags=separate,auto-detect,scans,multi-photo,organize
home.sign.title=Podpis
home.sign.desc=Dodaje podpis do dokumentu PDF za pomocą rysunku, tekstu lub obrazu
sign.tags=autoryzacja, inicjały, podpis odręczny, podpis tekstowy, podpis graficzny
sign.tags=authorize,initials,drawn-signature,text-sign,image-signature
home.flatten.title=Spłaszcz
home.flatten.desc=Usuń wszystkie interaktywne elementy i formularze z dokumentu PDF
flatten.tags=statyczny, dezaktywacja, nieinteraktywny, opływowy, streamline
flatten.tags=static,deactivate,non-interactive,streamline
home.repair.title=Napraw
home.repair.desc=Spróbuj naprawić uszkodzony dokument PDF
repair.tags=naprawianie, naprawa, przywracanie, poprawianie, odzyskiwanie
repair.tags=fix,restore,correction,recover
home.removeBlanks.title=Usuń puste strony
home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF
removeBlanks.tags=czyszczenie, usprawnianie, brak treści, organizowanie
removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Usuń notatki/przypisy
home.removeAnnotations.desc=Usuwa wszystkie notatki i przypisy z dokumentu PDF
removeAnnotations.tags=komentarze, podświetlanie, notatki, znaczniki, usuwanie
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Porównaj
home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF
compare.tags=rozróżnienie, kontrast, zmiany, analiza
compare.tags=differentiate,contrast,changes,analysis
home.certSign.title=Podpisz certyfikatem
home.certSign.desc=Podpisz dokument PDF za pomocą certyfikatu/klucza prywatnego (PEM/P12)
certSign.tags=uwierzytelnianie, PEM, P12, oficjalny, szyfrowanie
certSign.tags=authenticate,PEM,P12,official,encrypt
home.removeCertSign.title=Usuń podpis certyfikatem
home.removeCertSign.desc=Usuń podpis certyfikatem z dokumentu PDF
removeCertSign.tags=uwierzytelnianie, PEM, P12, oficjalny, odszyfrowywanie
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=Układ wielu stron
home.pageLayout.desc=Scal wiele stron dokumentu PDF w jedną stronę
pageLayout.tags=scalanie, kompozycja, pojedynczy widok, organizowanie, porządkowanie
pageLayout.tags=merge,composite,single-view,organize
home.scalePages.title=Dopasuj rozmiar stron
home.scalePages.desc=Dopasuj rozmiar stron wybranego dokumentu PDF
scalePages.tags=zmiana rozmiaru, modyfikacja, rozmiar, dostosowanie
scalePages.tags=resize,modify,dimension,adapt
home.pipeline.title=Automatyzacja
home.pipeline.desc=Wykonaj wiele akcji na dokumentach PDF, tworząc automatyzację
pipeline.tags=automatyzacja, sekwencja, skrypt, przetwarzanie wsadowe
pipeline.tags=automate,sequence,scripted,batch-process
home.add-page-numbers.title=Dodaj numery stron
home.add-page-numbers.desc=Dodaj numery strony w dokumencie PDF w podanej lokalizacji
add-page-numbers.tags=stronicowanie, etykieta, organizowanie, indeks, index
add-page-numbers.tags=paginate,label,organize,index
home.auto-rename.title=Automatycznie zmień nazwę PDF
home.auto-rename.desc=Automatycznie zmień nazwę PDF bazując na nagłówku
auto-rename.tags=automatyczne wykrywanie, oparte na nagłówkach, organizowanie, ponowne etykietowanie
auto-rename.tags=auto-detect,header-based,organize,relabel
home.adjust-contrast.title=Zmień kolor/nasycenie/jasność
home.adjust-contrast.desc=Zmień kolor/nasycenie/jasność w dokumencie PDF
adjust-contrast.tags=Korekcja kolorów, dostrajanie, modyfikacja, ulepszanie
adjust-contrast.tags=color-correction,tune,modify,enhance
home.crop.title=Przytnij PDF
home.crop.desc=Przytnij dokument PDF w celu zmniejszenia rozmiaru
crop.tags=przycinanie, zmniejszanie, edycja, kształtowanie
crop.tags=trim,shrink,edit,shape
home.autoSplitPDF.title=Automatycznie podziel strony
home.autoSplitPDF.desc=Automatycznie podziel dokument na strony
autoSplitPDF.tags=Oparty na QR, rozdzielanie, skanowanie, organizowanie
autoSplitPDF.tags=QR-based,separate,scan-segment,organize
home.sanitizePdf.title=Dezynfekcja
home.sanitizePdf.desc=Usuń skrypt i inne elementy z dokumentu PDF
sanitizePdf.tags=czyszczenie, ochrona, bezpieczeństwo, usuwanie zagrożeń
sanitizePdf.tags=clean,secure,safe,remove-threats
home.URLToPDF.title=Strona WWW do PDFa
home.URLToPDF.desc=Zapisuje podany adres WWW do PDFa
URLToPDF.tags=przechwytywanie stron internetowych, zapisywanie strony, strona internetowa do dokumentu, archiwizacja
URLToPDF.tags=web-capture,save-page,web-to-doc,archive
home.HTMLToPDF.title=HTML do PDF
home.HTMLToPDF.desc=Zapisuje podany plik HTML/ZIP do PDF
HTMLToPDF.tags=znaczniki, treść internetowa, transformacja, konwertowanie
HTMLToPDF.tags=markup,web-content,transformation,convert
home.MarkdownToPDF.title=Markdown do PDF
home.MarkdownToPDF.desc=Zapisuje dokument Markdown do PDF
MarkdownToPDF.tags=znaczniki, treść internetowa, transformacja, konwertowanie
MarkdownToPDF.tags=markup,web-content,transformation,convert
home.getPdfInfo.title=Pobierz informacje o pliku PDF
home.getPdfInfo.desc=Pobiera wszelkie informacje o pliku PDF
getPdfInfo.tags=informacje, dane, statystyka, statystyki
getPdfInfo.tags=infomation,data,stats,statistics
home.extractPage.title=Wyciągnij stronę z PDF
home.extractPage.desc=Wyciąga stronę z dokumentu PDF
extractPage.tags=wydobycie,separacja,wyciaganie
extractPage.tags=extract
home.PdfToSinglePage.title=PDF do jednej strony
home.PdfToSinglePage.desc=Łączy wszystkie strony PDFa w jedną wielką stronę PDF
PdfToSinglePage.tags=pojedyncza strona
PdfToSinglePage.tags=single page
home.showJS.title=Pokaż kod JavaScript
@@ -465,66 +465,66 @@ showJS.tags=JS
home.autoRedact.title=Zaciemnij
home.autoRedact.desc=Zaciemnia dokument PDF bazując na podanej wartości
autoRedact.tags=Redagowanie, ukrywanie, zaciemnianie, zaczernianie, zaznaczanie, ukrywanie
autoRedact.tags=Redact,Hide,black out,black,marker,hidden
home.tableExtraxt.title=PDF do CSV
home.tableExtraxt.desc=Konwertuje tabele z PDF do pliku CSV
tableExtraxt.tags=CSV, ekstrakcja tabeli, ekstrakcja, konwersja, wydobywanie
tableExtraxt.tags=CSV,Table Extraction,extract,convert
home.autoSizeSplitPDF.title=Podziel (Rozmiar/Ilość stron)
home.autoSizeSplitPDF.desc=Rozdziela dokument PDF na wiele dokumentów bazując na podanym rozmiarze, ilości stron bądź ilości dokumentów
autoSizeSplitPDF.tags=pdf, dzielenie, dokument, organizacja
autoSizeSplitPDF.tags=pdf,split,document,organization
home.overlay-pdfs.title=Nałóż PDFa
home.overlay-pdfs.desc=Nakłada dokumenty PDF na siebie
overlay-pdfs.tags=Nakładka
overlay-pdfs.tags=Overlay
home.split-by-sections.title=Podziel PDF na sekcje
home.split-by-sections.desc=Podziel strony PDF w mniejsze sekcje
split-by-sections.tags=Podział sekcji, dzielenie, dostosowywanie
split-by-sections.tags=Section Split, Divide, Customize
home.AddStampRequest.title=Dodaj pieczęć
home.AddStampRequest.desc=Dodaj pieczęć tekstową/obrazową w wyznaczonej lokalizacji dokumentu
AddStampRequest.tags=Stempel, dodawanie obrazu, wyśrodkowanie obrazu, znak wodny, PDF, osadzanie, dostosowywanie
AddStampRequest.tags=Stamp, Add image, center image, Watermark, PDF, Embed, Customize
home.PDFToBook.title=PDF do eBooka
home.PDFToBook.desc=Zapisuje dokument PDF w formacie eBooka za pomocą Calibre
PDFToBook.tags=Książka,komiks,Calibre, konwertowanie, manga, amazon, kindle
PDFToBook.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.BookToPDF.title=eBook do PDF
home.BookToPDF.desc=Zapisuje ebooka do PDF za pomocą Calibre
BookToPDF.tags=Book,Comic,Calibre,Convert,manga,amazon,kindle
home.removeImagePdf.title=Usuń obraz
home.removeImagePdf.desc=Usuń obraz z pliku PDF, aby zmniejszyć rozmiar pliku
removeImagePdf.tags=Usuń obraz, operacje na stronie, back-end, strona serwera
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=Podziel PDF według rozdziałów
home.splitPdfByChapters.desc=Podział pliku PDF na wiele plików na podstawie struktury rozdziałów.
splitPdfByChapters.tags=podział, rozdziały, zakładki, porządkowanie, organizacja
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=Zamień-Odwróć-Kolor
replace-color.header=Zamień-Odwróć kolor PDF
home.replaceColorPdf.title=Zastąp i Odwróć Kolor
home.replaceColorPdf.desc=Zastąp kolor tekstu i tła w pliku PDF i odwróć pełen kolor pliku PDF, aby zmniejszyć rozmiar pliku
replaceColorPdf.tags=Zastąp kolor, operacje na stronach, back-end, strona serwera
replace-color.selectText.1=Zastąp lub Odwróć opcje kolorów
replace-color.selectText.2=Domyślnie (domyślne kolory o wysokim kontraście)
replace-color.selectText.3=Niestandardowe (kolory niestandardowe)
replace-color.selectText.4=Całkowita-Odwrotność (Odwrócenie wszystkich kolorów)
replace-color.selectText.5=Wysoki kontrast opcji kolorystycznych
replace-color.selectText.6=biały tekst na czarnym tle
replace-color.selectText.7=Czarny tekst na białym tle
replace-color.selectText.8=Żółty tekst na czarnym tle
replace-color.selectText.9=Zielony tekst na czarnym tle
replace-color.selectText.10=Wybierz Kolor tekstu
replace-color.selectText.11=Wybierz Kolor tła
replace-color.submit=Zamień
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
@@ -543,17 +543,18 @@ login.locked=Konto jest zablokowane
login.signinTitle=Zaloguj się
login.ssoSignIn=Zaloguj się za pomocą logowania jednokrotnego
login.oauth2AutoCreateDisabled=Wyłączono automatyczne tworzenie użytkownika OAUTH2
login.oauth2AdminBlockedUser=Rejestracja lub logowanie niezarejestrowanych użytkowników jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=Błąd logowania OAuth2
login.oauth2InvalidUserInfoResponse=Niewłaściwe dane logowania
login.oauth2invalidRequest=Nieprawidłowe żądanie
login.oauth2AccessDenied=Brak dostępu
login.oauth2InvalidTokenResponse=Nieprawidłowa odpowiedź na token
login.oauth2InvalidIdToken=Nieprawidłowa wartość tokenu
login.userIsDisabled=Użytkownik jest nieaktywny, logowanie przy użyciu tej nazwy użytkownika jest obecnie zablokowane. Prosimy o kontakt z administratorem.
login.alreadyLoggedIn=Jesteś już zalogowany na
login.alreadyLoggedIn2=urządzeniach. Wyloguj się z tych urządzeń i spróbuj ponownie.
login.toManySessions=Masz zbyt wiele aktywnych sesji
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatyczne zaciemnienie
@@ -777,8 +778,8 @@ removeAnnotations.submit=Usuń
#compare
compare.title=Porównaj
compare.header=Porównaj PDF(y)
compare.highlightColor.1=Kolor Podświetlenia 1:
compare.highlightColor.2=Kolor Podświetlenia 2:
compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Color 2:
compare.document.1=Dokument 1
compare.document.2=Dokument 2
compare.submit=Porównaj
@@ -830,7 +831,7 @@ ScannerImageSplit.selectText.7=Minimalny obszar konturu:
ScannerImageSplit.selectText.8=Ustawia próg minimalnego obszaru konturu dla zdjęcia
ScannerImageSplit.selectText.9=Rozmiar obramowania:
ScannerImageSplit.selectText.10=Ustawia rozmiar dodawanego i usuwanego obramowania, aby uniknąć białych obramowań na wyjściu (domyślnie: 1).
ScannerImageSplit.info=Python nie został zainstalowany. Jest on wymagany do uruchomienia.
ScannerImageSplit.info=Python is not installed. It is required to run.
#OCR
@@ -857,7 +858,7 @@ ocr.submit=Przetwarzaj PDF za pomocą OCR
extractImages.title=Wyodrębnij obrazy
extractImages.header=Wyodrębnij obrazy
extractImages.selectText=Wybierz format obrazu, na który chcesz przekonwertować wyodrębniony obraz.
extractImages.allowDuplicates=Zapisz zduplikowane obrazy
extractImages.allowDuplicates=Save duplicate images
extractImages.submit=Wyodrębnij
@@ -918,8 +919,8 @@ pdfOrganiser.placeholder=(przykład 1,3,2 lub 4-8,2,10-12 lub 2n-1)
#multiTool
multiTool.title=Narzędzie Wielofunkcyjne PDF
multiTool.header=Narzędzie Wielofunkcyjne PDF
multiTool.title=Multi narzędzie PDF
multiTool.header=Multi narzędzie PDF
multiTool.uploadPrompts=Nazwa pliku
#view pdf
@@ -982,7 +983,7 @@ pdfToImage.color=Kolor
pdfToImage.grey=Odcień szarości
pdfToImage.blackwhite=Czarno-biały (może spowodować utratę danych!)
pdfToImage.submit=Konwertuj
pdfToImage.info=Python nie został zainstalowany. Jest wymagany do konwersji WebP.
pdfToImage.info=Python is not installed. Required for WebP conversion.
#addPassword
@@ -1019,7 +1020,7 @@ watermark.selectText.6=Odstęp w pionie (odstęp między każdym znakiem wodnym
watermark.selectText.7=Nieprzezroczystość (0% - 100%):
watermark.selectText.8=Typ znaku wodnego:
watermark.selectText.9=Obraz znaku wodnego:
watermark.selectText.10=Konwertuj PDF do PDF-Image
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=Dodaj znak wodny
watermark.type.1=Tekst
watermark.type.2=Obraz
@@ -1119,7 +1120,7 @@ PDFToXML.submit=Konwertuj
#PDFToCSV
PDFToCSV.title=PDF na CSV
PDFToCSV.header=PDF na CSV
PDFToCSV.prompt=Wybierz stronę do wyodrębnienia tabeli
PDFToCSV.prompt=Choose page to extract table
PDFToCSV.submit=Zatwierdź
#split-by-size-or-count
@@ -1181,8 +1182,8 @@ licenses.license=Licencja
survey.nav=Ankieta
survey.title=Ankieta Stirling-PDF
survey.description=Stirling-PDF nie śledzi swoich użytkowników, więc chciałby poznać opinie swoich użytkowników!
survey.changes=Stirling-PDF zmieniło się od czasu ostatniej ankiety! Aby dowiedzieć się więcej, sprawdź nasz wpis na blogu tutaj:
survey.changes2=Dzięki tym zmianom otrzymujemy płatne wsparcie biznesowe i finansowanie
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.please=Wypełnij proszę ankietę dla nas!
survey.disabled=(Blokada wyskakującego okienka z ankieta zostanie dodane w następnych aktualizacjach, ale będzie dostępna na dole strony)
survey.button=Wypełnij ankietę
@@ -1204,21 +1205,21 @@ error.discordSubmit=Discord - wyślij posta z prośbą o pomoc
#remove-image
removeImage.title=Usuń obraz
removeImage.header=Usuń obraz
removeImage.removeImage=Usuń obraz
removeImage.submit=Usuń obraz
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image
splitByChapters.title=Podziel PDF według Rozdziałów
splitByChapters.header=Podziel PDF według Rozdziałów
splitByChapters.bookmarkLevel=Poziom Zakładek
splitByChapters.includeMetadata=Dołącz Metadane
splitByChapters.allowDuplicates=Zezwalaj na Duplikaty
splitByChapters.desc.1=Narzędzie to dzieli plik PDF na wiele plików PDF w oparciu o strukturę rozdziałów.
splitByChapters.desc.2=Poziom Zakładek: Wybierz poziom zakładek, który ma zostać użyty do podziału (0 dla najwyższego poziomu, 1 dla drugiego poziomu itd.).
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
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF

View File

@@ -3,42 +3,42 @@
###########
# the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr
addPageNumbers.fontSize=Tamanho da Fonte
addPageNumbers.fontName=Nome da Fonte
addPageNumbers.fontSize=Tamanho da fonte
addPageNumbers.fontName=Nome da fonte
pdfPrompt=Selecione PDF(s)
multiPdfPrompt=Selecione PDFs (2+)
multiPdfDropPrompt=Selecione (ou arraste e solte) todos os PDFs desejados
imgPrompt=Selecione a(s) Imagem(ns)
multiPdfDropPrompt=Selecione (ou arraste e solte) todos os PDFs necessários
imgPrompt=Selecione a(s) imagem(ns)
genericSubmit=Enviar
processTimeWarning=Aviso: esse processo pode levar até um minuto, dependendo do tamanho do arquivo
pageOrderPrompt=Ordem de Página Personalizada (Digite uma lista de números de páginas, separadas por vírgula ou Funções como 2n+1):
pageSelectionPrompt=Seleção de Página Personalizada (Digite uma lista de números de páginas, separadas por vírgula 1,5,6 ou Funções como 2n+1) :
pageOrderPrompt=Ordem de página personalizada (digite uma lista de números de página separada por vírgula):
pageSelectionPrompt=Seleção de página personalizada (digite uma lista de números de página separada por vírgula 1,5,6 ou Funções como 2n+1) :
goToPage=Ir
true=Verdadeiro
false=Falso
unknown=Desconhecido
save=Salvar
saveToBrowser=Salvar no Navegador
saveToBrowser=Salvar no navegador
close=Fechar
filesSelected=Arquivos Selecionados
noFavourites=Nenhum Favorito Adicionado
filesSelected=arquivos selecionados
noFavourites=Nenhum favorito adicionado
downloadComplete=Download Completo
bored=Entediado Esperando?
bored=Entediado esperando?
alphabet=Alfabeto
downloadPdf=Baixar PDF
text=Texto
font=Fonte
selectFillter=-- Selecione --
pageNum=Número da Página
pageNum=Número da página
sizes.small=Pequeno
sizes.medium=Médio
sizes.large=Grande
sizes.x-large=Extra grande
error.pdfPassword=O documento PDF está protegido por senha e a senha não foi fornecida ou está incorreta
delete=Apagar
delete=apagar
username=Usuário
password=Senha
welcome=Bem-vindo
welcome=Bem vindo
property=Propriedade
black=Preto
white=Branco
@@ -46,7 +46,7 @@ red=Vermelho
green=Verde
blue=Azul
custom=Personalizado...
WorkInProgess=Trabalho em progresso, Talvez não funcione ou apresente erros, Por favor, reporte qualquer problema!
WorkInProgess=Em progesso, Talvez não funcione ou apresente erros, Por favor, reporte qualquer problema!
poweredBy=Distribuído por
yes=Sim
no=Não
@@ -55,30 +55,30 @@ notAuthenticatedMessage=Usuário não autenticado.
userNotFoundMessage=Usuário não encontrado.
incorrectPasswordMessage=A senha atual está incorreta.
usernameExistsMessage=Novo Usuário já existe.
invalidUsernameMessage=Usuário inválido, nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um e-mail válido.
invalidUsernameMessage=Usuário inválido, nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um email válido.
invalidPasswordMessage=A senha não deve estar vazia e não deve conter espaços no início ou no final.
confirmPasswordErrorMessage=Nova Senha e Confirmar Nova Senha devem ser iguais.
deleteCurrentUserMessage=Não é possível apagar usuário conectado no momento.
deleteCurrentUserMessage=Não é possível apagar o usuário da sessão atual.
deleteUsernameExistsMessage=O usuário não existe e não pode ser apagado.
downgradeCurrentUserMessage=Não é possível fazer downgrade da função do usuário conectado no momento.
disabledCurrentUserMessage=O usuário atual não pode ser desativado.
downgradeCurrentUserMessage=Não é possível fazer downgrade da função do usuário atual
disabledCurrentUserMessage=O usuário atual não pode ser desativado
downgradeCurrentUserLongMessage=Não é possível fazer downgrade da função do usuário atual. Portanto, o usuário atual não será mostrado.
userAlreadyExistsOAuthMessage=O usuário já existe como um usuário OAuth2.
userAlreadyExistsWebMessage=O usuário já existe como um usuário Web.
userAlreadyExistsWebMessage=O usuário já existe como um usuário web.
error=Erro
oops=Ops!
help=Ajuda
goHomepage=Ir para a Página Inicial
joinDiscord=Junte-se ao nosso servidor Discord
seeDockerHub=Visite o Docker Hub
visitGithub=Visite o repositório no GitHub
visitGithub=Visite o repositŕio no GitHub
donate=Doar
color=Cor
sponsor=Patrocinador
sponsor=Patrocine
info=Informações
pro=Pro
page=Página
pages=Páginas
page=Page
pages=Pages
legal.privacy=Política de Privacidade
legal.terms=Termos e Condições
@@ -90,7 +90,7 @@ legal.impressum=Informações legais
# Pipeline #
###############
pipeline.header=Menu do Pipeline (Beta)
pipeline.uploadButton=Upload Personalizado
pipeline.uploadButton=Upload personalizado
pipeline.configureButton=Configurar
pipeline.defaultOption=Personalizado
pipeline.submitButton=Enviar
@@ -103,10 +103,10 @@ pipeline.deletePrompt=Tem certeza de que deseja excluir o pipeline
######################
pipelineOptions.header=Configuração do Pipeline
pipelineOptions.pipelineNameLabel=Nome do Pipeline
pipelineOptions.saveSettings=Salvar Configurações da Operação
pipelineOptions.saveSettings=Salvar configurações da operação
pipelineOptions.pipelineNamePrompt=Insira o nome do pipeline aqui
pipelineOptions.selectOperation=Selecione uma Operação
pipelineOptions.addOperationButton=Adicione uma Operação
pipelineOptions.selectOperation=Selecione uma operação
pipelineOptions.addOperationButton=Adicione uma operação
pipelineOptions.pipelineHeader=Pipeline:
pipelineOptions.saveButton=Baixar
pipelineOptions.validateButton=Validar
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=Validar
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Atualize para versão Pro
enterpriseEdition.warning=Esse recurso só está disponivel para usuários da versão Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro suporta arquivos de configuração YAML e outros recursos SSO.
enterpriseEdition.ssoAdvert=Procurando por mais recursos de controle de usuários? Veja Stirling PDF Pro
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=Você quer melhorar Stirling PDF?
analytics.paragraph1=Stirling PDF possui coleta de dados opcional para ajudar a melhorar o produto. Nós não rastreamos nenhuma informação pessoal ou conteúdo dos arquivos.
analytics.paragraph2=Por favor considere habilitar coleta de dados para ajudar Stirling-PDF a crescer e nos ajudar a entender nossos usuários melhor.
analytics.enable=Habilitar coleta de dados
analytics.disable=Desabilitar coleta de dados
analytics.settings=Você pode alterar as configurações de coleta de dados no arquivo config/settings.yml
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@@ -162,61 +162,61 @@ settings.zipThreshold=Compactar arquivos quando o número de arquivos baixados e
settings.signOut=Sair
settings.accountSettings=Configurações de conta
settings.bored.help=Habilitar jogos secretos
settings.cacheInputs.name=Salvar entradas do formulário
settings.cacheInputs.name=Salvar entradas de formulário
settings.cacheInputs.help=Habilitar para armazenar entradas usadas anteriormente para execuções futuras
changeCreds.title=Alterar Credenciais
changeCreds.header=Atualizar Detalhes da Conta
changeCreds.title=Alterar credenciais
changeCreds.header=Atualizar detalhes da sua conta
changeCreds.changePassword=Você está usando as credenciais padrões. Por favor, insira uma nova senha
changeCreds.newUsername=Novo Usuário
changeCreds.oldPassword=Senha Atual
changeCreds.newPassword=Senha Nova
changeCreds.confirmNewPassword=Confirme a Nova Senha
changeCreds.submit=Enviar Alterações
changeCreds.newUsername=Novo usuário
changeCreds.oldPassword=Senha atual
changeCreds.newPassword=Nova senha
changeCreds.confirmNewPassword=Confirmar nova senha
changeCreds.submit=Enviar alterações
account.title=Configurações de Conta
account.accountSettings=Configurações de Conta
account.adminSettings=Configurações de Administrador Visualizar e Adicionar Usuários
account.userControlSettings=Configurações de Controle de Usuário
account.changeUsername=Alterar Usuário
account.newUsername=Novo Usuário
account.password=Senha de Confirmação
account.oldPassword=Senha Antiga
account.newPassword=Senha Nova
account.changePassword=Alterar a Senha
account.confirmNewPassword=Confirme a Nova Senha
account.title=Configurações de conta
account.accountSettings=Configurações de conta
account.adminSettings=Configurações de Administrador visualizar e adicionar usuários
account.userControlSettings=Configurações de controle de usuário
account.changeUsername=Alterar usuário
account.newUsername=Novo usuário
account.password=Senha de confirmação
account.oldPassword=Senha antiga
account.newPassword=Senha nova
account.changePassword=Alterar a senha
account.confirmNewPassword=Confirme a nova senha
account.signOut=Sair
account.yourApiKey=Sua chave de API
account.syncTitle=Sincronize as configurações do navegador com a conta
account.settingsCompare=Comparação de Configurações:
account.settingsCompare=Comparação de configurações:
account.property=Propriedade
account.webBrowserSettings=Configuração do navegador Web
account.syncToBrowser=Sincronizar Conta -> Navegador
account.syncToAccount=Sincronizar Conta <- Navegador
account.syncToBrowser=Sincronizar conta -> Navegador
account.syncToAccount=Sincronizar conta <- navegador
adminUserSettings.title=Configurações de controle de usuário
adminUserSettings.header=Configurações de controle do usuário administrador
adminUserSettings.header=Configurações de controle de usuário administrador
adminUserSettings.admin=Administrador
adminUserSettings.user=Usuário
adminUserSettings.addUser=Adicionar novo usuário
adminUserSettings.deleteUser=Apagar usuário
adminUserSettings.confirmDeleteUser=O usuário deve ser apagado?
adminUserSettings.confirmChangeUserStatus=O usuário deve ser desabilitado/habilitado?
adminUserSettings.usernameInfo=Nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um e-mail válido.
adminUserSettings.usernameInfo=Nome de usuário só pode incluir letras, números e os seguintes caracteres especiais @._+- ou deve ser um email válido.
adminUserSettings.roles=Funções
adminUserSettings.role=Função
adminUserSettings.actions=Ações
adminUserSettings.apiUser=Usuário de API limitado
adminUserSettings.extraApiUser=Usuário de API limitado adicional
adminUserSettings.webOnlyUser=Usuário web apenas
adminUserSettings.webOnlyUser=Usuário apenas web
adminUserSettings.demoUser=Usuário demo (Sem configurações personalizadas)
adminUserSettings.internalApiUser=Usuário interno de API
adminUserSettings.forceChange=Forçar usuário a trocar a senha ao iniciar sessão
adminUserSettings.submit=Salvar Usuário
adminUserSettings.changeUserRole=Alterar Função do Usuário
adminUserSettings.submit=Salvar usuário
adminUserSettings.changeUserRole=Alterar Função de Usuário
adminUserSettings.authenticated=Autenticado
adminUserSettings.editOwnProfil=Editar próprio perfil
adminUserSettings.enabledUser=usuário habilitado
@@ -235,15 +235,15 @@ database.fileSize=Tamanho do arquivo
database.deleteBackupFile=Apagar arquivo de backup
database.importBackupFile=Importar arquivo de backup
database.downloadBackupFile=Baixar arquivo de backup
database.info_1=Ao importar dados, é crucial garantir a estrutura correta. Se você não tem certeza do que está fazendo procure auxílio de um profissional. Um erro na estrutura pode ocasionar em mau funcionamento da aplicação, incluindo a impossibilidade da aplicação ser executada.
database.info_1=Ao importar dados, é crucial garantir a estrutura correta. Se você não tem certeza do que está fazendo procure auxílio de um profissional. Um erro na estrutura pode ocasionar em mau funcionamento da aplicação, incluindo a possibilidade de perda total da aplicação.
database.info_2=O nome do arquivo não importa ao enviar. Ele será renomeado em seguida para seguir o formato backup_user_yyyyMMddHHmm.sql, garantindo uma convenção de nomes coerente.
database.submit=Importar Backup
database.importIntoDatabaseSuccessed=Importação para o banco de dados bem sucedida
database.fileNotFound=Arquivo não encontrado
database.fileNullOrEmpty=O arquivo não pode estar nulo ou vazio
database.fileNullOrEmpty=O arquivo não estar nulo ou vazio
database.failedImportFile=Falha ao importar arquivo
session.expired=Sua sessão expirou. Por gentileza atualize a página e tente novamente.
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
@@ -257,11 +257,11 @@ home.viewPdf.desc=Visualizar, anotar, adicionar texto ou imagens
viewPdf.tags=visualizar,ler,anotar,texto,imagem
home.multiTool.title=Multiferramenta de PDF
home.multiTool.desc=Mesclar, girar, reorganizar, dividir e remover páginas
multiTool.tags=Multiferramenta, múltiplas operações, Interface do Usuário, Clique e arraste, front-end, lado do cliente, interativo, intratável, movimento, excluir, migrar, dividir
home.multiTool.desc=Mesclar, girar, reorganizar e remover páginas
multiTool.tags=Multiferramenta,Múltiplas operações,Interface do Usuário,Clique e arraste,front-end,lado do cliente,interativo,intratável,movimento
home.merge.title=Mesclar
home.merge.desc=Mescle facilmente vários PDFs em um só.
home.merge.desc=Mesclar facilmente vários PDFs em um só.
merge.tags=mesclar,Operações de Página,Back-end,lado do servidor
home.split.title=Dividir
@@ -269,7 +269,7 @@ home.split.desc=Dividir PDFs em vários documentos
split.tags=Operações de Página,dividir,Múltiplas Páginas,cortar,lado do servidor
home.rotate.title=Girar
home.rotate.desc=Gire facilmente seus PDFs.
home.rotate.desc=Girar facilmente seus PDFs.
rotate.tags=Lado do servidor
@@ -287,11 +287,11 @@ pdfOrganiser.tags=duplex,par,ímpar,ordenar,mover
home.addImage.title=Adicionar Imagem
home.addImage.desc=Adicionar uma imagem em um local definido no PDF
home.addImage.desc=Adicionar uma imagem em um local definido no PDF (Em desenvolvimento)
addImage.tags=img,jpg,imagem,foto
home.watermark.title=Adicionar Marca d'água
home.watermark.desc=Adicionar uma marca d'água personalizada ao seu documento PDF.
home.watermark.title="Adicionar Marca d'água"
home.watermark.desc="Adicionar uma marca d'água personalizada ao seu documento PDF."
watermark.tags=Texto,repetindo,rótulo,próprio,direitos autorais,marca registrada,img,jpg,imagem,foto
home.permissions.title=Alterar Permissões
@@ -303,13 +303,13 @@ home.removePages.title=Remover
home.removePages.desc=Excluir as páginas indesejadas do seu documento PDF.
removePages.tags=Remover páginas,excluir páginas
home.addPassword.title=Adicionar Senha
home.addPassword.title=Adicionar senha
home.addPassword.desc=Criptografar seu documento PDF com uma senha.
addPassword.tags=seguro,segurança
home.removePassword.title=Remover Senha
home.removePassword.desc=Remover a proteção por senha do seu documento PDF.
removePassword.tags=seguro, descriptografar, segurança, remover senha
removePassword.tags=seguro, Descriptografar, segurança, remover senha
home.compressPdfs.title=Comprimir
home.compressPdfs.desc=Comprimir PDFs para reduzir o tamanho do arquivo.
@@ -339,7 +339,7 @@ pdfToPDFA.tags=arquivo,longo prazo,padrão,conversão,armazenamento,preservaçã
home.PDFToWord.title=PDF para Word
home.PDFToWord.desc=Converter PDF para formatos Word (DOC, DOCX e ODT)
PDFToWord.tags=doc,docx,odt,word,transformação,formato,conversão,escritório,microsoft,doc
PDFToWord.tags=doc,docx,odt,word,transformação,formato,conversão,escritório,microsoft,arquivo doc
home.PDFToPresentation.title=PDF para Apresentação
home.PDFToPresentation.desc=Converter PDF para formatos de apresentação (PPT, PPTX e ODP)
@@ -355,12 +355,12 @@ PDFToHTML.tags=conteúdo web,compatível com navegador
home.PDFToXML.title=PDF para XML
home.PDFToXML.desc=Converter PDF para formato XML
home.PDFToXML.desc=Converter PDF para o formato XML
PDFToXML.tags=extração-de-dados,conteúdo-estruturado,interoperabilidade,transformação,converter
home.ScannerImageSplit.title=Detectar/Dividir Fotos Digitalizadas
home.ScannerImageSplit.desc=Divide várias fotos de dentro de uma imagem/PDF
ScannerImageSplit.tags=separar,detecção-automática,digitalizações,fotos-múltiplas,organizar
home.ScannerImageSplit.desc=Divide várias fotos de dentro de uma imagem/PDF digitalizado
ScannerImageSplit.tags=separar,detecção-automática,digitalizações,foto-múltipla,organizar
home.sign.title=Assinar
home.sign.desc=Adicionar assinatura ao PDF por desenho, texto ou imagem
@@ -378,7 +378,7 @@ home.removeBlanks.title=Remover Páginas em Branco
home.removeBlanks.desc=Detectar e remover páginas em branco de um documento
removeBlanks.tags=limpeza,otimização,sem-conteúdo,organizar
home.removeAnnotations.title=Remover Anotações
home.removeAnnotations.title=Remover anotações
home.removeAnnotations.desc=Remove todos os comentários/anotações de um PDF
removeAnnotations.tags=comentários,destaque,notas,marcação,remover
@@ -426,7 +426,7 @@ home.autoSplitPDF.title=Divisão Automática de Páginas
home.autoSplitPDF.desc=Dividir automaticamente um PDF digitalizado com separador de páginas físicas QR Code
autoSplitPDF.tags=baseado-em-QR,separar,segmento-de-digitalização,organizar
home.sanitizePdf.title=Higienizar
home.sanitizePdf.title=Sanitizar
home.sanitizePdf.desc=Remover scripts e outros elementos de arquivos PDF
sanitizePdf.tags=limpar,seguro,protegido,remover-ameaças
@@ -468,7 +468,7 @@ home.autoRedact.desc=Ocultação automática (escurecimento) de texto em um PDF
autoRedact.tags=Redigir,ocultar,escurecer,preto,marcador,oculto
home.tableExtraxt.title=PDF para CSV
home.tableExtraxt.desc=Extrai tabelas de um PDF convertendo para CSV
home.tableExtraxt.desc=Extrai tabelas de um PDF convertendo-o para CSV
tableExtraxt.tags=CSV,extração de tabela,extrair,converter
@@ -481,13 +481,13 @@ home.overlay-pdfs.title=Sobrepor PDFs
home.overlay-pdfs.desc=Sobrepõe PDFs sobre outro PDF
overlay-pdfs.tags=Sobreposição
home.split-by-sections.title=Dividir PDF por Seções
home.split-by-sections.title=Dividir PDF por seções
home.split-by-sections.desc=Divida cada página de um PDF em seções horizontais e verticais menores
split-by-sections.tags=Seção Dividir, Dividir, Personalizar
home.AddStampRequest.title=Adicionar Carimbo ao PDF
home.AddStampRequest.title=Adicionar carimbo ao PDF
home.AddStampRequest.desc=Adicione texto ou carimbos de imagem em locais definidos
AddStampRequest.tags=Carimbo,Adicionar imagem,centralizar imagem,Marca d'água,PDF,Incorporar,Personalizar
AddStampRequest.tags="Carimbo,Adicionar imagem,centralizar imagem,Marca d'água,PDF,Incorporar,Personalizar"
home.PDFToBook.title=PDF para Livro
@@ -495,36 +495,36 @@ home.PDFToBook.desc=Converte PDF para formatos de livro/quadrinhos usando Calibr
PDFToBook.tags=Livro,Quadrinhos,Calibre,Converter,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.BookToPDF.title=Livro para PDF
home.BookToPDF.desc=Converte formatos de livros/quadrinhos para PDF usando Calibre
home.BookToPDF.desc=Converte formatos de livros/quadrinhos em PDF usando Calibre
BookToPDF.tags=Livro,Quadrinhos,Calibre,Converter,manga,amazon,kindle,epub,mobi,azw3,docx,rtf,txt,html,lit,fb2,pdb,lrf
home.removeImagePdf.title=Remover Imagem
home.removeImagePdf.title=Remover imagem
home.removeImagePdf.desc=Remova a imagem do PDF para reduzir o tamanho do arquivo
removeImagePdf.tags=Remover imagem,operações de página,back-end,lado do servidor
home.splitPdfByChapters.title=Divide PDF por Capítulos
home.splitPdfByChapters.desc=Divide um PDF em vários arquivos baseado na sua estrutura de capítulos.
splitPdfByChapters.tags=dividir,capítulos,favoritos,organizar
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=Substituir-Inverter-Cor
replace-color.header=Substitui-Inverter Cor PDF
home.replaceColorPdf.title=Substitui e Inverte Cor
home.replaceColorPdf.desc=Substitui cor de um texto e plano de fundo de um PDF e inverte a toda cor do PDF para reduzir o tamanho
replaceColorPdf.tags=Substitui Cor, Operações na Página, back end, lado do servidor
replace-color.selectText.1=Substituir ou inverter cor Opções
replace-color.selectText.2=Padrão(Padrão cores de alto constraste)
replace-color.selectText.3=Customizado(Cores customizadas)
replace-color.selectText.4=Inversão Completa(Inverte todas cores)
replace-color.selectText.5=Opções de cores de alto contraste
replace-color.selectText.6=Texto branco em um plano de fundo preto
replace-color.selectText.7=Texto preto em um plano de fundo branco
replace-color.selectText.8=Texto amarelo em um plano de fundo preto
replace-color.selectText.9=Texto verde em um plano de fundo preto
replace-color.selectText.10=Escolha cor do texto
replace-color.selectText.11=Escolha cor do plano de fundo
replace-color.submit=Substituir
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
@@ -541,23 +541,24 @@ login.rememberme=Lembrar de mim
login.invalid=Usuário ou senha inválidos.
login.locked=Sua conta foi bloqueada.
login.signinTitle=Por favor, inicie a sessão
login.ssoSignIn=Iniciar sessão através de login único (SSO)
login.ssoSignIn=Iniciar sessão através de início de sessão único
login.oauth2AutoCreateDisabled=Auto-Criar Usuário OAUTH2 Desativado
login.oauth2AdminBlockedUser=O registro ou login de usuários não registrados está atualmente bloqueado. Entre em contato com o administrador.
login.oauth2RequestNotFound=Solicitação de autorização não encontrada
login.oauth2InvalidUserInfoResponse=Resposta de informação de usuário inválida
login.oauth2invalidRequest=Requisição Inválida
login.oauth2AccessDenied=Acesso Negado
login.oauth2InvalidTokenResponse=Resposta de Token Inválida
login.oauth2InvalidIdToken=Id de Token Inválido
login.oauth2invalidRequest=Requisição inválida
login.oauth2AccessDenied=Acesso negado
login.oauth2InvalidTokenResponse=Resposta de token inválida
login.oauth2InvalidIdToken=Id de token inválido
login.userIsDisabled=O usuário está desativado, o login está atualmente bloqueado com este nome de usuário. Entre em contato com o administrador.
login.alreadyLoggedIn=Você já está conectado
login.alreadyLoggedIn2=aparelhos. Por favor saia dos aparelhos e tente novamente.
login.toManySessions=Você tem muitas sessões ativas
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Redação Automática de Dados
autoRedact.header=Redação Automática de Dados
autoRedact.title=Redigir automaticamente
autoRedact.header=Redigir automaticamente
autoRedact.colorLabel=Cor
autoRedact.textsToRedactLabel=Texto para redigir (separado por linha)
autoRedact.textsToRedactPlaceholder=por exemplo: \nConfidencial \nSecreto
@@ -643,22 +644,22 @@ AddStampRequest.fontSize=Tamanho da fonte/imagem
AddStampRequest.rotation=Rotação
AddStampRequest.opacity=Opacidade
AddStampRequest.position=Posição
AddStampRequest.overrideX=Substituir Coordenada X
AddStampRequest.overrideY=Substituir Coordenada Y
AddStampRequest.overrideX=Substituir coordenada X
AddStampRequest.overrideY=Substituir coordenada Y
AddStampRequest.customMargin=Margem personalizada
AddStampRequest.customColor=Cor de texto personalizada
AddStampRequest.submit=Enviar
#sanitizePDF
sanitizePDF.title=Higienizar PDF
sanitizePDF.header=Higienizar um arquivo PDF
sanitizePDF.title=Sanitizar PDF
sanitizePDF.header=Sanitizar um arquivo PDF
sanitizePDF.selectText.1=Remover ações de JavaScript
sanitizePDF.selectText.2=Remover arquivos embutidos
sanitizePDF.selectText.3=Remover metadados
sanitizePDF.selectText.4=Remover links
sanitizePDF.selectText.5=Remover fontes
sanitizePDF.submit=Higienizar PDF
sanitizePDF.submit=Sanitizar PDF
#addPageNumbers
@@ -738,9 +739,9 @@ certSign.title=Assinatura com Certificado
certSign.header=Assine um PDF com o seu certificado (Em desenvolvimento)
certSign.selectPDF=Selecione um arquivo PDF para assinatura:
certSign.jksNote=Nota: Se o seu tipo de certificado não estiver listado abaixo, converta-o em um arquivo Java Keystore (.jks) usando a ferramenta de linha de comando keytool. Em seguida, escolha a opção de arquivo .jks abaixo.
certSign.selectKey=Selecione o seu arquivo de chave privada (formato PKCS#8, pode ser .pem ou .der):
certSign.selectKey="Selecione o seu arquivo de chave privada (formato PKCS#8, pode ser .pem ou .der):"
certSign.selectCert=Selecione o seu arquivo de certificado (formato X.509, pode ser .pem ou .der):
certSign.selectP12=Selecione o seu arquivo de armazenamento de chave PKCS#12 (.p12 ou .pfx) (opcional, se fornecido, deve conter a sua chave privada e certificado):
certSign.selectP12="Selecione o seu arquivo de armazenamento de chave PKCS#12 (.p12 ou .pfx) (opcional, se fornecido, deve conter a sua chave privada e certificado):"
certSign.selectJKS=Selecione seu arquivo Java Keystore (.jks ou .keystore):
certSign.certType=Tipo de Certificado
certSign.password=Digite a senha do seu armazenamento de chave ou chave privada (se aplicável):
@@ -752,7 +753,7 @@ certSign.submit=Assinar PDF
#removeCertSign
removeCertSign.title=Remover Assinatura do Certificado
removeCertSign.title=Remover assinatura com Certificado
removeCertSign.header=Remover o certificado digital do PDF
removeCertSign.selectPDF=Selecione um arquivo PDF:
removeCertSign.submit=Remover assinatura
@@ -764,13 +765,13 @@ removeBlanks.header=Remover páginas em branco
removeBlanks.threshold=Limite de brancura de pixel:
removeBlanks.thresholdDesc=Limite para determinar o quão branco um pixel branco deve ser para ser classificado como 'Branco'. 0 = Preto, 255 branco puro.
removeBlanks.whitePercent=Porcentagem de Branco (%):
removeBlanks.whitePercentDesc=Porcentagem da página que devem ter pixels “brancos” para serem removidas
removeBlanks.whitePercentDesc=Porcentagem de páginas que devem ter pixels “brancos” para serem removidas
removeBlanks.submit=Remover páginas em branco
#removeAnnotations
removeAnnotations.title=Remover Anotações
removeAnnotations.header=Remover Anotações
removeAnnotations.title=Remover anotações
removeAnnotations.header=Remover anotações
removeAnnotations.submit=Remover
@@ -836,26 +837,26 @@ ScannerImageSplit.info=Python não está instalado. É necessário para executar
#OCR
ocr.title=OCR / Limpeza de Digitalização
ocr.header=OCR / Limpeza de Digitalização (Reconhecimento Óptico de Caracteres)
ocr.selectText.1=Selecione os idiomas a serem detectados no PDF (os listados são os atualmente instalados):
ocr.selectText.2=Criar um arquivo de texto contendo o texto OCR junto do PDF com OCR
ocr.selectText.1=Selecione os idiomas a serem detectados no PDF (os listados são os atualmente detectados):
ocr.selectText.2=Criar um arquivo de texto contendo o texto OCR ao lado do PDF com OCR
ocr.selectText.3=Páginas corretamente digitalizadas em um ângulo inclinado, gire-as de volta à posição original
ocr.selectText.4=Limpar a página para reduzir a probabilidade de o OCR encontrar texto no ruído de fundo (sem alteração na saída)
ocr.selectText.5=Limpar a página para reduzir a probabilidade de o OCR encontrar texto no ruído de fundo, mantendo a limpeza na saída.
ocr.selectText.6=Ignorar páginas com texto interativo, processar por OCR apenas as páginas com imagens
ocr.selectText.6=Ignorar páginas com texto interativo, processar apenas as páginas de OCR que são imagens
ocr.selectText.7=Forçar OCR, executar OCR em todas as páginas, removendo todos os elementos de texto originais
ocr.selectText.8=Normal (gerará um erro se o PDF já contiver texto)
ocr.selectText.9=Configurações Adicionais
ocr.selectText.9=Configurações adicionais
ocr.selectText.10=Modo OCR
ocr.selectText.11=Remover imagens após o OCR (remove TODAS as imagens, útil apenas como parte do processo de conversão)
ocr.selectText.12=Tipo de Renderização (avançado)
ocr.selectText.12=Tipo de renderização (avançado)
ocr.help=Por favor, leia a documentação sobre como usar isso para outros idiomas e/ou fora do ambiente Docker
ocr.credit=Este serviço usa OCRmyPDF e Tesseract para OCR.
ocr.submit=Processar PDF com OCR
#extractImages
extractImages.title=Extrair Imagens
extractImages.header=Extrair Imagens
extractImages.title=Extrair imagens
extractImages.header=Extrair imagens
extractImages.selectText=Selecione o formato de imagem para converter as imagens extraídas
extractImages.allowDuplicates=Salvar imagens duplicadas
extractImages.submit=Extrair
@@ -883,7 +884,7 @@ compress.submit=Comprimir
#Add image
addImage.title=Adicionar Imagem
addImage.title=Adicionar imagem
addImage.header=Adicionar imagem ao PDF
addImage.everyPage=Para cada página?
addImage.upload=Enviar imagem
@@ -892,7 +893,7 @@ addImage.submit=Adicionar imagem
#merge
merge.title=Mesclar
merge.header=Mesclar vários PDFs (2+)
merge.header=Mesclar Vários PDFs (2+)
merge.sortByName=Classificar por nome
merge.sortByDate=Classificar por data
merge.removeCertSign=Remover a assinatura digital do arquivo mesclado?
@@ -906,7 +907,7 @@ pdfOrganiser.submit=Reorganizar páginas
pdfOrganiser.mode=Modo
pdfOrganiser.mode.1=Ordem de página personalizada
pdfOrganiser.mode.2=Ordem inversa
pdfOrganiser.mode.3=Classificação duplex
pdfOrganiser.mode.3=Classificação Duplex
pdfOrganiser.mode.4=Classificação de livreto
pdfOrganiser.mode.5=Classificação de livreto com ponto lateral
pdfOrganiser.mode.6=Divisão ímpar-par
@@ -971,7 +972,7 @@ imageToPDF.selectText.5=Converter em PDFs separados
#pdfToImage
pdfToImage.title=PDF para Imagem
pdfToImage.title=PDF para imagem
pdfToImage.header=Converter PDF para imagem
pdfToImage.selectText=Formato de imagem
pdfToImage.singleOrMultiple=Tipo de resultado de imagem
@@ -980,7 +981,7 @@ pdfToImage.multi=Várias imagens, uma imagem por página
pdfToImage.colorType=Tipo de cor
pdfToImage.color=Colorida
pdfToImage.grey=Escala de Cinza
pdfToImage.blackwhite=Preto e Branco (pode perder informações!)
pdfToImage.blackwhite=Preto e Branco (pode perder de dados!)
pdfToImage.submit=Converter
pdfToImage.info=Python não está instalado. Necessário para conversão WebP.
@@ -991,7 +992,7 @@ addPassword.header=Adicionar Senha (Criptografar)
addPassword.selectText.1=Selecione o PDF para Criptografar
addPassword.selectText.2=Senha
addPassword.selectText.3=Tamanho da Chave de Criptografia
addPassword.selectText.4=Valores mais altos são mais seguros, mas valores mais baixos são melhores para compatibilidade.
addPassword.selectText.4=Valores mais altos são mais seguros, mas valores mais baixos são mais compatíveis.
addPassword.selectText.5=Permissões a serem definidas (recomendado para uso junto com a senha do proprietário)
addPassword.selectText.6=Impedir a montagem do documento
addPassword.selectText.7=Impedir a extração de conteúdo
@@ -1008,26 +1009,26 @@ addPassword.submit=Criptografar
#watermark
watermark.title=Adicionar marca d'água
watermark.header=Adicionar marca d'água
watermark.selectText.1=Selecione PDF para adicionar a marca d'água:
watermark.selectText.2=Texto da marca d'água:
watermark.title="Adicionar marca d'água"
watermark.header="Adicionar marca d'água"
watermark.selectText.1="Selecione PDF para adicionar a marca d'água:"
watermark.selectText.2="Texto da marca d'água:"
watermark.selectText.3=Tamanho da fonte:
watermark.selectText.4=Rotação (0-360):
watermark.selectText.5=widthSpacer (Espaço entre cada marca d'água horizontalmente):
watermark.selectText.6=heightSpacer (Espaço entre cada marca d'água verticalmente):
watermark.selectText.5="widthSpacer (Espaço entre cada marca d'água horizontalmente):"
watermark.selectText.6="heightSpacer (Espaço entre cada marca d'água verticalmente):"
watermark.selectText.7=Opacidade (0% - 100%):
watermark.selectText.8=Tipo de marca d'água:
watermark.selectText.9=Imagem da marca d'água:
watermark.selectText.8="Tipo de marca d'água:"
watermark.selectText.9="Imagem da marca d'água:"
watermark.selectText.10=Converter PDF em imagem PDF
watermark.submit=Adicionar marca d'água
watermark.submit="Adicionar marca d'água"
watermark.type.1=Texto
watermark.type.2=Imagem
#Change permissions
permissions.title=Alterar Permissões
permissions.header=Alterar Permissões
permissions.title=Alterar permissões
permissions.header=Alterar permissões
permissions.warning=Aviso: para que essas permissões sejam imutáveis, é recomendável defini-las com uma senha através da página Adicionar Senha
permissions.selectText.1=Selecione o PDF para alterar as permissões
permissions.selectText.2=Permissões para definir
@@ -1043,7 +1044,7 @@ permissions.submit=Alterar
#remove password
removePassword.title=Remover Senha
removePassword.title=Remover senha
removePassword.header=Remover senha (descriptografar)
removePassword.selectText.1=Selecione o PDF para descriptografar
removePassword.selectText.2=Senha
@@ -1051,7 +1052,7 @@ removePassword.submit=Remover
#changeMetadata
changeMetadata.title=Alterar Metadados
changeMetadata.title=Alterar metadados
changeMetadata.header=Alterar metadados
changeMetadata.selectText.1=Edite as variáveis que deseja alterar
changeMetadata.selectText.2=Excluir todos os metadados
@@ -1135,7 +1136,7 @@ split-by-size-or-count.submit=Enviar
#overlay-pdfs
overlay-pdfs.header=Sobrepor Arquivos PDF
overlay-pdfs.header=Sobrepor arquivos PDF
overlay-pdfs.baseFile.label=Selecione o arquivo PDF base
overlay-pdfs.overlayFiles.label=Selecione os arquivos PDF para sobreposição
overlay-pdfs.mode.label=Selecione o modo de sobreposição
@@ -1153,7 +1154,7 @@ overlay-pdfs.submit=Enviar
#split-by-sections
split-by-sections.title=Dividir PDF por seções
split-by-sections.header=Divida o PDF em seções
split-by-sections.horizontal.label=Divisões Horizontais
split-by-sections.horizontal.label=Divisões horizontais
split-by-sections.vertical.label=Divisões Verticais
split-by-sections.horizontal.placeholder=Insira o número de divisões horizontais
split-by-sections.vertical.placeholder=Insira o número de divisões verticais
@@ -1171,8 +1172,8 @@ printFile.submit=Imprimir
#licenses
licenses.nav=Licenças
licenses.title=Licenças de Terceiros
licenses.header=Licenças de Terceiros
licenses.title=Licenças de terceiros
licenses.header=Licenças de terceiros
licenses.module=Módulo
licenses.version=Versão
licenses.license=Licença
@@ -1181,10 +1182,10 @@ licenses.license=Licença
survey.nav=Pesquisa
survey.title=Pesquisa Stirling-PDF
survey.description=Stirling-PDF não tem rastreamento, então queremos ouvir nossos usuários para melhorar o Stirling-PDF!
survey.changes=Stirling-PDF mudou desde o a última pesquisa! Para saber mais acesse nosso post no blog:
survey.changes2=Com essas mudanças estamos implementando suporte empresarial pago e financeamento
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.please=Por favor, considere responder à nossa pesquisa!
survey.disabled=(O pop-up da pesquisa será desativado nas atualizações seguintes, mas estará disponível no rodapé da página)
survey.disabled=(O pop-up da pesquisa será desativado nas atualizações seguintes, mas estará disponível no final da página)
survey.button=Responda a pesquisa
survey.dontShowAgain=Não mostre novamente
@@ -1192,11 +1193,11 @@ survey.dontShowAgain=Não mostre novamente
#error
error.sorry=Desculpe pelo problema!
error.needHelp=Precisa de ajuda / Encontrou um problema?
error.contactTip=Se você ainda estiver com problemas, não hesite em entrar em contato conosco para obter ajuda. Você pode enviar um tíquete em nossa página GitHub ou entrar em contato conosco através do Discord:
error.contactTip=Se você ainda estiver com problemas, não hesite em entrar em contato conosco para obter ajuda. Você pode enviar um ticket em nossa página GitHub ou entrar em contato conosco através do Discord:
error.404.head=404 - Página não encontrada | Ops, tropeçamos no código!
error.404.1=Não conseguimos encontrar a página que você está procurando.
error.404.2=Algo deu errado
error.github=Submeter um tíquete no GitHub
error.github=Submeter um ticket no GitHub
error.showStack=Mostrar rastreamento de pilha
error.copyStack=Copiar rastreamento de pilha
error.githubSubmit=GitHub - Submeter um tíquete
@@ -1204,21 +1205,21 @@ error.discordSubmit=Discord - Submeter um post de suporte
#remove-image
removeImage.title=Remover Imagem
removeImage.title=Remover imagem
removeImage.header=Remover imagem
removeImage.removeImage=Remover imagem
removeImage.submit=Remover imagem
splitByChapters.title=Dividir PDF por Capítulos
splitByChapters.header=Dividir PDF por Capítulos
splitByChapters.bookmarkLevel=Nível de Marcador
splitByChapters.includeMetadata=Incluir Metadados
splitByChapters.allowDuplicates=Permitir Cópias
splitByChapters.desc.1=Essa ferramenta divide um arquivo PDF em vários arquivos PDFs baseado na estrutura de cápitulos.
splitByChapters.desc.2=Nível de Marcador: Escolha o nível de marcador a ser usado para divisão (0 para o primeiro nível, 1 para o segundo nível, etc).
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
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Edição Automática

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=Utilizatorul este dezactivat, conectarea este în prezent b
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Redactare Automată

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Автоматическое редактирование

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Automatické redigovanie

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto Cenzura

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=Användaren är inaktiverad, inloggning är för närvarand
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Auto-redigera

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=ซ่อนข้อมูลอัตโนมัติ

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=Kullanıcı devre dışı bırakıldı, şu anda bu kullan
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Otomatik Karartma

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Автоматичне редагування

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=User is deactivated, login is currently blocked with this u
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=Tự động biên tập

View File

@@ -554,6 +554,7 @@ login.userIsDisabled=用户被禁用,登录已被阻止。请联系管理员
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=自动删除

View File

@@ -3,8 +3,8 @@
###########
# the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr
addPageNumbers.fontSize=字型大小
addPageNumbers.fontName=字型名稱
addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name
pdfPrompt=選擇 PDF 檔案
multiPdfPrompt=選擇多個 PDF 檔案
multiPdfDropPrompt=選擇(或拖放)所有需要的 PDF 檔案
@@ -12,7 +12,7 @@ imgPrompt=選擇圖片
genericSubmit=送出
processTimeWarning=警告:此過程可能長達一分鐘,具體取決於檔案大小
pageOrderPrompt=自訂頁面順序(輸入以逗號分隔的頁碼或函式,如 2n+1
pageSelectionPrompt=自訂頁面選擇(輸入以逗號分隔的頁碼 1、5、6 或 2n+1 等函的清單):
pageSelectionPrompt=自訂頁面選擇(輸入以逗號分隔的頁碼 1、5、6 或 2n+1 等函的清單):
goToPage=前往
true=
false=
@@ -21,7 +21,7 @@ save=儲存
saveToBrowser=儲存到瀏覽器
close=關閉
filesSelected=已選擇的檔案
noFavourites=還沒有功能被收藏
noFavourites=未新增收藏
downloadComplete=下載完成
bored=等待時覺得無聊?
alphabet=字母表
@@ -51,62 +51,62 @@ poweredBy=Powered by
yes=
no=
changedCredsMessage=憑證已變更!
notAuthenticatedMessage=使用者未通過驗證。
notAuthenticatedMessage=使用者未證。
userNotFoundMessage=找不到使用者。
incorrectPasswordMessage=目前密碼不正確。
usernameExistsMessage=新使用者名稱已存在。
invalidUsernameMessage=無效的使用者名稱使用者名稱只能包含字母、數字和以下特殊字元 @._+- 或必須是有效的電子郵件地址。
invalidPasswordMessage=密碼不能為空,且開頭和結尾不能有空格。
confirmPasswordErrorMessage=新密碼與確認密碼必須相符
deleteCurrentUserMessage=無法刪除目前登的使用者。
deleteUsernameExistsMessage=使用者名不存在,無法刪除。
invalidUsernameMessage=使用者名稱無效,使用者名稱只能包含字母、數字和以下特殊字元 @._+- 或必須是有效的電子郵件地址。
invalidPasswordMessage=The password must not be empty and must not have spaces at the beginning or end.
confirmPasswordErrorMessage=輸入的密碼必需和確認密碼相同
deleteCurrentUserMessage=無法刪除目前登的使用者。
deleteUsernameExistsMessage=使用者名不存在,無法刪除。
downgradeCurrentUserMessage=無法降級目前使用者的角色
disabledCurrentUserMessage=無法停用目前使用者
downgradeCurrentUserLongMessage=無法降級目前使用者的角色。因此,不會顯示目前使用者。
userAlreadyExistsOAuthMessage=使用者已經以 OAuth2 使用者身份存在
userAlreadyExistsWebMessage=使用者已經以網頁使用者身份存在
disabledCurrentUserMessage=The current user cannot be disabled
downgradeCurrentUserLongMessage=無法降級目前使用者的角色。因此,不會顯示目前使用者。
userAlreadyExistsOAuthMessage=使用者已 OAuth2 註冊
userAlreadyExistsWebMessage=使用者已於網頁註冊
error=錯誤
oops=哎呀!
help=說明
help=幫助
goHomepage=前往首頁
joinDiscord=加入我們的 Discord 伺服器
seeDockerHub=造訪 Docker Hub 儲存庫
visitGithub=訪 GitHub 專案
donate=
seeDockerHub=查看 Docker Hub
visitGithub= GitHub 存儲庫
donate=
color=顏色
sponsor=贊助
info=資訊
pro=專業版
page=頁面
pages=頁面
info=Info
pro=Pro
page=Page
pages=Pages
legal.privacy=隱私權政策
legal.terms=使用條款
legal.accessibility=無障礙性聲明
legal.cookie=Cookie 政策
legal.impressum=版本說明
legal.privacy=Privacy Policy
legal.terms=Terms and Conditions
legal.accessibility=Accessibility
legal.cookie=Cookie Policy
legal.impressum=Impressum
###############
# Pipeline #
###############
pipeline.header=管道功能選單(測試版)
pipeline.uploadButton=上傳自訂設
pipeline.configureButton=設定
pipeline.header=管道選單(測試版)
pipeline.uploadButton=上傳自定
pipeline.configureButton=配置
pipeline.defaultOption=自訂
pipeline.submitButton=送出
pipeline.help=管道功能說
pipeline.scanHelp=資料夾掃描
pipeline.deletePrompt=確定刪除管道
pipeline.help=管道
pipeline.scanHelp=資料夾掃描
pipeline.deletePrompt=確定刪除管道?
######################
# Pipeline Options #
######################
pipelineOptions.header=管道設定
pipelineOptions.header=管道配置
pipelineOptions.pipelineNameLabel=管道名稱
pipelineOptions.saveSettings=存操作設
pipelineOptions.pipelineNamePrompt=在此輸入管道名稱
pipelineOptions.saveSettings=存操作設
pipelineOptions.pipelineNamePrompt=在此輸入管道名稱
pipelineOptions.selectOperation=選擇操作
pipelineOptions.addOperationButton=新增操作
pipelineOptions.addOperationButton=添加操作
pipelineOptions.pipelineHeader=管道:
pipelineOptions.saveButton=下載
pipelineOptions.validateButton=驗證
@@ -114,21 +114,21 @@ pipelineOptions.validateButton=驗證
########################
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=升級至專業版
enterpriseEdition.warning=此功能僅提供給專業版使用者使用。
enterpriseEdition.yamlAdvert=Stirling PDF 專業版支援 YAML 設定檔和其他單一登入 (SSO) 功能。
enterpriseEdition.ssoAdvert=需要更多使用者管理功能嗎?請參考 Stirling PDF 專業版
enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
#################
# Analytics #
#################
analytics.title=您想協助改善 Stirling PDF 嗎?
analytics.paragraph1=Stirling PDF 有選擇性的分析功能,可幫助我們改進產品。我們不會追蹤任何個人資訊或檔案內容。
analytics.paragraph2=請考慮啟用分析功能,以協助 Stirling-PDF 成長並讓我們更了解使用者需求。
analytics.enable=啟用分析功能
analytics.disable=停用分析功能
analytics.settings=您可以在 config/settings.yml 檔案中變更分析功能的設定
analytics.title=Do you want make Stirling PDF better?
analytics.paragraph1=Stirling PDF has opt in analytics to help us improve the product. We do not track any personal information or file contents.
analytics.paragraph2=Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.
analytics.enable=Enable analytics
analytics.disable=Disable analytics
analytics.settings=You can change the settings for analytics in the config/settings.yml file
#############
# NAVBAR #
@@ -138,36 +138,36 @@ navbar.darkmode=深色模式
navbar.language=語言
navbar.settings=設定
navbar.allTools=工具
navbar.multiTool=多功能工具
navbar.sections.organize=整理
navbar.multiTool=萬用工具
navbar.sections.organize=組織
navbar.sections.convertTo=轉換為 PDF
navbar.sections.convertFrom=從 PDF 轉換
navbar.sections.security=簽章與安全性
navbar.sections.advance=進階
navbar.sections.edit=檢視與編輯
navbar.sections.popular=熱門功能
navbar.sections.popular=Popular
#############
# SETTINGS #
#############
settings.title=設定
settings.update=有更新可用
settings.updateAvailable=目前安裝的版本 {0}。有新版本({1})可供使用
settings.appVersion=應用程式版本:
settings.downloadOption.title=選擇下載選項(適用於單一檔案非壓縮下載):
settings.updateAvailable=當前版本 {0}。歡迎你更新至最新版 ({1})。
settings.appVersion=應用版本:
settings.downloadOption.title=選擇下載選項(於單一檔案非壓縮下載):
settings.downloadOption.1=在同一視窗中開啟
settings.downloadOption.2=在新視窗中開啟
settings.downloadOption.3=下載檔案
settings.zipThreshold=當下載檔案數量超過此數值時,將檔案壓縮
settings.zipThreshold=當下載檔案數量超過時,壓縮檔案
settings.signOut=登出
settings.accountSettings=設定
settings.bored.help=啟用彩蛋遊戲
settings.cacheInputs.name=儲存表單輸入
settings.cacheInputs.help=啟用此功能以儲存先前使用的輸入,以便日後使用
settings.accountSettings=設定
settings.bored.help=啟用彩蛋
settings.cacheInputs.name=輸入檔案下載
settings.cacheInputs.help=開啟記住先前的輸入,做為日後使用
changeCreds.title=變更憑證
changeCreds.header=更新的帳詳細資訊
changeCreds.changePassword=您正在使用預設登入憑證。請輸入新密碼
changeCreds.header=更新的帳詳細資訊
changeCreds.changePassword=你使用的是預設登錄認證。請輸入新密碼
changeCreds.newUsername=新使用者名稱
changeCreds.oldPassword=目前密碼
changeCreds.newPassword=新密碼
@@ -176,9 +176,9 @@ changeCreds.submit=送出變更
account.title=設定
account.accountSettings=設定
account.adminSettings=管理設定 - 檢視和新增使用者
account.title=設定
account.accountSettings=設定
account.adminSettings=管理設定 - 檢視和新增使用者
account.userControlSettings=使用者控制設定
account.changeUsername=修改使用者名稱
account.newUsername=新使用者名稱
@@ -188,43 +188,43 @@ account.newPassword=新密碼
account.changePassword=修改密碼
account.confirmNewPassword=確認新密碼
account.signOut=登出
account.yourApiKey=的 API 金鑰
account.syncTitle=將瀏覽器設定與帳同步
account.yourApiKey=的 API 金鑰
account.syncTitle=將瀏覽器設定與帳同步
account.settingsCompare=設定比較:
account.property=屬性
account.webBrowserSettings=網頁瀏覽器設定
account.syncToBrowser=同步帳 → 瀏覽器
account.syncToAccount=同步帳 ← 瀏覽器
account.syncToBrowser=同步帳 → 瀏覽器
account.syncToAccount=同步帳 ← 瀏覽器
adminUserSettings.title=使用者控制設定
adminUserSettings.header=管理使用者控制設定
adminUserSettings.header=管理使用者控制設定
adminUserSettings.admin=管理員
adminUserSettings.user=使用者
adminUserSettings.addUser=新增使用者
adminUserSettings.deleteUser=刪除使用者
adminUserSettings.confirmDeleteUser=定要刪除使用者?
adminUserSettings.confirmChangeUserStatus=是否要停用/啟用此使用者?
adminUserSettings.usernameInfo=使用者名稱只能包含字母、數字和以下特殊字元 @._+- 或必須是有效的電子郵件地址。
adminUserSettings.confirmDeleteUser=刪除使用者?
adminUserSettings.confirmChangeUserStatus=Should the user be disabled/enabled?
adminUserSettings.usernameInfo=使用者名稱只能包含字母、數字和以下特殊字元 @._+-或必須是有效的電子郵件地址。
adminUserSettings.roles=角色
adminUserSettings.role=角色
adminUserSettings.actions=操作
adminUserSettings.apiUser=受限制的 API 使用者
adminUserSettings.extraApiUser=額外受限制的 API 使用者
adminUserSettings.webOnlyUser=僅網頁使用者
adminUserSettings.demoUser=示範使用者(無自訂設定)
adminUserSettings.extraApiUser=其他受限 API 使用者
adminUserSettings.webOnlyUser=使用網頁使用者
adminUserSettings.demoUser=示範用途的使用者(無自訂設定)
adminUserSettings.internalApiUser=內部 API 使用者
adminUserSettings.forceChange=強制使用者在登入時變更密碼
adminUserSettings.submit=儲存使用者
adminUserSettings.changeUserRole=更使用者角色
adminUserSettings.forceChange=強制使用者在登入時修改使用者名稱/密碼
adminUserSettings.submit=儲存
adminUserSettings.changeUserRole=使用者身份
adminUserSettings.authenticated=已驗證
adminUserSettings.editOwnProfil=編輯自己的個人資料
adminUserSettings.enabledUser=已啟用使用者
adminUserSettings.disabledUser=已停用使用者
adminUserSettings.activeUsers=使用中的使用者:
adminUserSettings.disabledUsers=已停用的使用者:
adminUserSettings.totalUsers=使用者總數:
adminUserSettings.lastRequest=最後請求時間
adminUserSettings.editOwnProfil=Edit own profile
adminUserSettings.enabledUser=enabled user
adminUserSettings.disabledUser=disabled user
adminUserSettings.activeUsers=Active Users:
adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
database.title=資料庫匯入/匯出
@@ -235,20 +235,20 @@ database.fileSize=檔案大小
database.deleteBackupFile=刪除備份檔案
database.importBackupFile=匯入備份檔案
database.downloadBackupFile=下載備份檔案
database.info_1=在匯入資料時,確保正確的結構至關重要。如果不確定自己在做什麼,請尋求專業人士的建議和支。結構錯誤可能會導致應用程式故障,甚至完全無法執行應用程式
database.info_2=上傳時檔案名稱並不重要。上傳後將重新命名為 backup_user_yyyyMMddHHmm.sql 格式,以確保命名規範一致。
database.info_1=在匯入資料時,確保正確的結構至關重要。如果不確定自己在做什麼,請尋求專業人士的建議和支。結構錯誤可能會導致應用程式故障,甚至完全無法運行
database.info_2=上傳時檔案名稱無關緊要。上傳後將重新命名為 backup_user_yyyyMMddHHmm.sql 格式,以確保命名規範一致。
database.submit=匯入備份
database.importIntoDatabaseSuccessed=成功匯入資料庫
database.fileNotFound=找不到檔案
database.fileNullOrEmpty=檔案不為空或空白
database.fileNotFound=檔案未找到
database.fileNullOrEmpty=檔案不為空或空白
database.failedImportFile=匯入檔案失敗
session.expired=您的工作階段已過期。請重新整理頁面並再試一次。
session.expired=Your session has expired. Please refresh the page and try again.
#############
# HOME-PAGE #
#############
home.desc=你的本主機一站式 PDF 需求解決方案。
home.desc=你的本主機一站式 PDF 需求解決方案。
home.searchBar=搜尋功能...
@@ -256,9 +256,9 @@ home.viewPdf.title=檢視 PDF
home.viewPdf.desc=檢視、註釋、新增文字或圖片
viewPdf.tags=檢視,閱讀,註釋,文字,圖片
home.multiTool.title=PDF 複合工具
home.multiTool.title=PDF 工具
home.multiTool.desc=合併、旋轉、重新排列和移除頁面
multiTool.tags=複合工具,多功能,UI,點選拖,前端,客戶端,互動,互動,移動
multiTool.tags=工具,多操作,UI,點選拖,前端,客戶端,互動,互動,移動
home.merge.title=合併
home.merge.desc=輕鬆將多個 PDF 合併為一個。
@@ -392,7 +392,7 @@ certSign.tags=驗證,PEM,P12,官方,加密
home.removeCertSign.title=移除簽章
home.removeCertSign.desc=從 PDF 移除簽章
removeCertSign.tags=驗證,PEM,P12,官方,解密
removeCertSign.tags=authenticate,PEM,P12,official,decrypt
home.pageLayout.title=多頁面版面配置
home.pageLayout.desc=將 PDF 檔案的多個頁面合併到單一頁面
@@ -485,9 +485,9 @@ home.split-by-sections.title=依區段分割 PDF
home.split-by-sections.desc=將 PDF 的每一頁分割為較小的水平和垂直區段
split-by-sections.tags=區段分割, 劃分, 自訂
home.AddStampRequest.title=將圖章新增到 PDF
home.AddStampRequest.desc=在設位置新增文字或新增影像圖章
AddStampRequest.tags=圖章,新增圖片,中心像,浮水印,PDF,嵌入,自訂
home.AddStampRequest.title=將圖章添加到 PDF
home.AddStampRequest.desc=在設位置添加文字或添加圖像圖章
AddStampRequest.tags=圖章,添加圖片,中心像,浮水印,PDF,嵌入,自訂
home.PDFToBook.title=PDF 轉電子書
@@ -498,33 +498,33 @@ home.BookToPDF.title=電子書轉 PDF
home.BookToPDF.desc=使用 calibre 將書籍/漫畫格式轉換為 PDF
BookToPDF.tags=電子書,漫畫,Calibre,轉換,日本漫畫,亞馬遜,kindle
home.removeImagePdf.title=移除圖片
home.removeImagePdf.desc=從 PDF 中移除圖片以減少檔案大小
removeImagePdf.tags=移除圖片,頁面操作,後端,伺服器端
home.removeImagePdf.title=Remove image
home.removeImagePdf.desc=Remove image from PDF to reduce file size
removeImagePdf.tags=Remove Image,Page operations,Back end,server side
home.splitPdfByChapters.title=依章節分割 PDF
home.splitPdfByChapters.desc=根據 PDF 的章節結構將其分割成多個檔案。
splitPdfByChapters.tags=分割,章節,書籤,整理
home.splitPdfByChapters.title=Split PDF by Chapters
home.splitPdfByChapters.desc=Split a PDF into multiple files based on its chapter structure.
splitPdfByChapters.tags=split,chapters,bookmarks,organize
#replace-invert-color
replace-color.title=取代-反轉顏色
replace-color.header=取代-反轉 PDF 顏色
home.replaceColorPdf.title=取代與反轉顏色
home.replaceColorPdf.desc=取代 PDF 中文字和背景的顏色,並反轉整個 PDF 的顏色以減少檔案大小
replaceColorPdf.tags=取代顏色,頁面操作,後端,伺服器端
replace-color.selectText.1=取代或反轉顏色選項
replace-color.selectText.2=預設(預設高對比度顏色)
replace-color.selectText.3=自訂(自訂顏色)
replace-color.selectText.4=全部反轉(反轉所有顏色)
replace-color.selectText.5=高對比度顏色選項
replace-color.selectText.6=黑底白字
replace-color.selectText.7=白底黑字
replace-color.selectText.8=黑底黃字
replace-color.selectText.9=黑底綠字
replace-color.selectText.10=選擇文字顏色
replace-color.selectText.11=選擇背景顏色
replace-color.submit=取代
replace-color.title=Replace-Invert-Color
replace-color.header=Replace-Invert Color PDF
home.replaceColorPdf.title=Replace and Invert Color
home.replaceColorPdf.desc=Replace color for text and background in PDF and invert full color of pdf to reduce file size
replaceColorPdf.tags=Replace Color,Page operations,Back end,server side
replace-color.selectText.1=Replace or Invert color Options
replace-color.selectText.2=Default(Default high contrast colors)
replace-color.selectText.3=Custom(Customized colors)
replace-color.selectText.4=Full-Invert(Invert all colors)
replace-color.selectText.5=High contrast color options
replace-color.selectText.6=white text on black background
replace-color.selectText.7=Black text on white background
replace-color.selectText.8=Yellow text on black background
replace-color.selectText.9=Green text on black background
replace-color.selectText.10=Choose text Color
replace-color.selectText.11=Choose background Color
replace-color.submit=Replace
@@ -542,18 +542,19 @@ login.invalid=使用者名稱或密碼無效。
login.locked=你的帳戶已被鎖定。
login.signinTitle=請登入
login.ssoSignIn=透過織網單一簽入
login.oauth2AutoCreateDisabled=OAuth 2.0 自動建立使用者功能已停用
login.oauth2AdminBlockedUser=目前已封鎖非註冊使用者的註冊或登入。請聯絡系統管理員。
login.oauth2AutoCreateDisabled=OAuth 2.0 自動建立使用者已停用
login.oauth2AdminBlockedUser=Registration or logging in of non-registered users is currently blocked. Please contact the administrator.
login.oauth2RequestNotFound=找不到驗證請求
login.oauth2InvalidUserInfoResponse=無效的使用者資訊回應
login.oauth2invalidRequest=無效的回應
login.oauth2AccessDenied=存取被拒
login.oauth2InvalidTokenResponse=無效的權杖回應
login.oauth2InvalidIdToken=無效的識別權杖
login.userIsDisabled=使用者已停用,目前此使用者無法登入。請聯絡系統管理員。
login.alreadyLoggedIn=您已經登入了
login.alreadyLoggedIn2=個裝置。請登出其他裝置後再試一次。
login.toManySessions=您有太多使用中的工作階段
login.oauth2InvalidTokenResponse=無效的 Token 回應
login.oauth2InvalidIdToken=無效的 Tokne
login.userIsDisabled=User is deactivated, login is currently blocked with this username. Please contact the administrator.
login.alreadyLoggedIn=You are already logged in to
login.alreadyLoggedIn2=devices. Please log out of the devices and try again.
login.toManySessions=You have too many active sessions
login.toManySessions2=Please log out of the devices and try again. Alternatively, you can upgrade to Stirling PDF Pro.
#auto-redact
autoRedact.title=自動塗黑
@@ -639,7 +640,7 @@ AddStampRequest.stampType=圖章類型
AddStampRequest.stampText=圖章文字
AddStampRequest.stampImage=圖章圖片
AddStampRequest.alphabet=字母表
AddStampRequest.fontSize=型/影像大小
AddStampRequest.fontSize=體/圖像大小
AddStampRequest.rotation=旋轉
AddStampRequest.opacity=透明度
AddStampRequest.position=位置
@@ -728,7 +729,7 @@ pageLayout.submit=送出
scalePages.title=調整頁面大小/比例
scalePages.header=調整頁面大小/比例
scalePages.pageSize=文件的頁面大小。
scalePages.keepPageSize=原始大小
scalePages.keepPageSize=Original Size
scalePages.scaleFactor=頁面的縮放級別(裁剪)。
scalePages.submit=送出
@@ -737,7 +738,7 @@ scalePages.submit=送出
certSign.title=憑證簽章
certSign.header=使用你的憑證簽章(進行中)
certSign.selectPDF=選擇要簽章的 PDF 檔案:
certSign.jksNote=注意:如果你的證書類型未在下面列出,請使用 keytool 命令工具將其轉換為 Java Keystore .jks 檔。 然後,選擇下面的 .jks 文件選項。
certSign.jksNote=注意:如果你的證書類型未在下面列出,請使用 keytool 命令工具將其轉換為 Java Keystore .jks 檔。 然後,選擇下面的 .jks 文件選項。
certSign.selectKey=選擇你的私鑰文件PKCS#8 格式,可能是 .pem 或 .der
certSign.selectCert=選擇你的憑證文件X.509 格式,可能是 .pem 或 .der
certSign.selectP12=選擇你的 PKCS#12 金鑰庫文件(.p12 或 .pfx可選如果提供它應包含你的私鑰和憑證
@@ -777,8 +778,8 @@ removeAnnotations.submit=移除
#compare
compare.title=比較
compare.header=比較 PDF
compare.highlightColor.1=標示顏色 1
compare.highlightColor.2=標示顏色 2
compare.highlightColor.1=Highlight Color 1:
compare.highlightColor.2=Highlight Color 2:
compare.document.1=文件 1
compare.document.2=文件 2
compare.submit=比較
@@ -815,7 +816,7 @@ repair.submit=修復
#flatten
flatten.title=平坦化
flatten.header=PDF 平坦化
flatten.flattenOnlyForms=僅將表單平坦化
flatten.flattenOnlyForms=Flatten only forms
flatten.submit=平坦化
@@ -830,7 +831,7 @@ ScannerImageSplit.selectText.7=最小輪廓區域:
ScannerImageSplit.selectText.8=設定照片的最小輪廓區域閾值
ScannerImageSplit.selectText.9=邊框大小:
ScannerImageSplit.selectText.10=設定新增和移除的邊框大小以防止輸出中的白色邊框預設1
ScannerImageSplit.info=尚未安裝 Python。需要安裝 Python 才能執行。
ScannerImageSplit.info=Python is not installed. It is required to run.
#OCR
@@ -839,8 +840,8 @@ ocr.header=清理掃描 / OCR光學字元識別
ocr.selectText.1=選擇要在 PDF 中偵測的語言(列出的是目前可以偵測的語言):
ocr.selectText.2=產生包含 OCR 文字的文字文件,並與 OCR 的 PDF 一起
ocr.selectText.3=修正掃描的頁面傾斜角度,將它們旋轉回原位
ocr.selectText.4=清理頁面以降低 OCR 在背景雜訊中識別文字的機率。(無輸出變化)
ocr.selectText.5=清理頁面以降低 OCR 在背景雜訊中識別文字的機率,保持乾淨的輸出。
ocr.selectText.4=清理頁面,使 OCR 不太可能在背景噪音中找到文字。(無輸出變化)
ocr.selectText.5=清理頁面,使 OCR 不太可能在背景噪音中找到文字,保持清理的輸出。
ocr.selectText.6=忽略具有互動文字的頁面,只對影像頁面進行 OCR
ocr.selectText.7=強制 OCR將對每一頁進行 OCR移除所有原始文字元素
ocr.selectText.8=正常(如果 PDF 包含文字將出錯)
@@ -857,7 +858,7 @@ ocr.submit=使用 OCR 處理 PDF
extractImages.title=提取圖片
extractImages.header=提取圖片
extractImages.selectText=選擇要轉換提取影像的影像格式
extractImages.allowDuplicates=儲存重複的圖片
extractImages.allowDuplicates=Save duplicate images
extractImages.submit=提取
@@ -875,10 +876,10 @@ compress.title=壓縮
compress.header=壓縮 PDF
compress.credit=此服務使用 Ghostscript 進行 PDF 壓縮/最佳化。
compress.selectText.1=手動模式 - 從 1 到 4
compress.selectText.2=最佳化級:
compress.selectText.3=4對於含有文字影像來說結果很糟
compress.selectText.4=自動模式 - 自動調整品質使 PDF 達到指定的檔案大小
compress.selectText.5=指定的 PDF 檔案大小(例如 25MB, 10.8MB, 25KB
compress.selectText.2=最佳化級
compress.selectText.3=4對於文字影像非常糟糕
compress.selectText.4=自動模式 - 自動調整品質使 PDF 達到確定大小
compress.selectText.5=預期的 PDF 大小(例如 25MB, 10.8MB, 25KB
compress.submit=壓縮
@@ -913,14 +914,14 @@ pdfOrganiser.mode.6=奇偶拆分
pdfOrganiser.mode.7=刪除第一頁
pdfOrganiser.mode.8=刪除最後一頁
pdfOrganiser.mode.9=刪除第一頁和最後一頁
pdfOrganiser.mode.10=奇偶合併
pdfOrganiser.mode.10=Odd-Even Merge
pdfOrganiser.placeholder=(例如 1,3,2 或 4-8,2,10-12 或 2n-1
#multiTool
multiTool.title=PDF 複合工具
multiTool.header=PDF 複合工具
multiTool.uploadPrompts=檔名
multiTool.title=PDF 工具
multiTool.header=PDF 工具
multiTool.uploadPrompts=File Name
#view pdf
viewPdf.title=檢視 PDF
@@ -982,7 +983,7 @@ pdfToImage.color=顏色
pdfToImage.grey=灰度
pdfToImage.blackwhite=黑白(可能會遺失資料!)
pdfToImage.submit=轉換
pdfToImage.info=尚未安裝 Python。需要安裝 Python 才能進行 WebP 轉換。
pdfToImage.info=Python is not installed. Required for WebP conversion.
#addPassword
@@ -1019,7 +1020,7 @@ watermark.selectText.6=heightSpacer每個浮水印之間的垂直間距
watermark.selectText.7=不透明度0% - 100%
watermark.selectText.8=浮水印類型:
watermark.selectText.9=浮水印影像:
watermark.selectText.10= PDF 轉換為 PDF 影像
watermark.selectText.10=Convert PDF to PDF-Image
watermark.submit=新增浮水印
watermark.type.1=文字
watermark.type.2=圖片
@@ -1170,55 +1171,55 @@ printFile.submit=列印
#licenses
licenses.nav=授權條款
licenses.title=第三方授權條款
licenses.header=第三方授權條款
licenses.nav=許可證
licenses.title=第三方許可證
licenses.header=第三方許可證
licenses.module=模組
licenses.version=版本
licenses.license=授權條款
licenses.license=許可證
#survey
survey.nav=問卷調查
survey.title=Stirling-PDF 問卷調查
survey.description=Stirling-PDF 沒有追蹤功能,因此我們希望聽取使用者的意見來改進 Stirling-PDF
survey.changes=Stirling-PDF 自上次調查以來已有所改變!欲了解更多資訊,請查看我們的部落格文章:
survey.changes2=隨著這些變更,我們正在獲得付費的商業支援和資金
survey.please=請考慮參我們的問卷調查!
survey.disabled=(問卷調查彈出窗將在後續更新中停用,但仍可在頁使用)
survey.button=問卷調查
survey.description=Stirling-PDF 沒有追蹤功能,所以我們希望聽取用戶的意見來改進 Stirling-PDF
survey.changes=Stirling-PDF has changed since the last survey! To find out more please check our blog post here:
survey.changes2=With these changes we are getting paid business support and funding
survey.please=請考慮參我們的問卷調查!
survey.disabled=(問卷調查彈出窗將在後續更新中停用,但仍可在頁腳處使用)
survey.button=問卷調查
survey.dontShowAgain=不要再次顯示
#error
error.sorry=很抱歉造成您的困擾
error.needHelp=需要協助或發現問題?
error.contactTip=如果仍然遇到問題,請不要猶豫,隨時向我們尋求助。可以在我們的 GitHub 頁面回報問題,或過 Discord 我們聯
error.404.head=404 - 找不到頁面 | 糟糕,我們在程式碼中迷路了
error.404.1=我們似乎找不到正在尋找的頁面。
error.404.2=發生了一些錯誤
error.github=在 GitHub 上回報問題
error.sorry=對於這個問題,我們感到抱歉
error.needHelp=需要幫助/發現了一個問題?
error.contactTip=如果仍然遇到問題,請不要猶豫,隨時向我們尋求助。可以在我們的 GitHub 頁面提交工單,或過 Discord 我們聯
error.404.head=404 - 找不到頁面 | 哎呀,我們在代碼中走錯了路
error.404.1=我們好像找不到正在尋找的頁面。
error.404.2=出了點錯誤
error.github=在 GitHub 上提交工單
error.showStack=顯示堆疊追蹤
error.copyStack=複製堆疊追蹤
error.githubSubmit=GitHub - 回報問題
error.discordSubmit=Discord - 發表支援文章
error.githubSubmit=GitHub - 提交工單
error.discordSubmit=Discord - 提交支援帖子
#remove-image
removeImage.title=移除圖片
removeImage.header=移除圖片
removeImage.removeImage=移除圖片
removeImage.submit=移除圖片
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image
splitByChapters.title=依章節分割 PDF
splitByChapters.header=依章節分割 PDF
splitByChapters.bookmarkLevel=書籤層級
splitByChapters.includeMetadata=包含中繼資料
splitByChapters.allowDuplicates=允許重複
splitByChapters.desc.1=此工具會根據 PDF 檔案的章節結構將其分割成多個 PDF。
splitByChapters.desc.2=書籤層級選擇用於分割的書籤層級0 表示最上層1 表示第二層,依此類推)。
splitByChapters.desc.3=包含中繼資料:如果勾選,原始 PDF 的中繼資料將包含在每個分割後的 PDF 中。
splitByChapters.desc.4=允許重複:如果勾選,允許同一頁面上的多個書籤建立獨立的 PDF
splitByChapters.submit=分割 PDF
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
splitByChapters.includeMetadata=Include Metadata
splitByChapters.allowDuplicates=Allow Duplicates
splitByChapters.desc.1=This tool splits a PDF file into multiple PDFs based on its chapter structure.
splitByChapters.desc.2=Bookmark Level: Choose the level of bookmarks to use for splitting (0 for top-level, 1 for second-level, etc.).
splitByChapters.desc.3=Include Metadata: If checked, the original PDF's metadata will be included in each split PDF.
splitByChapters.desc.4=Allow Duplicates: If checked, allows multiple bookmarks on the same page to create separate PDFs.
splitByChapters.submit=Split PDF

View File

@@ -47,31 +47,20 @@ security:
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
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
idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml
idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml
idpIssuer: http://www.okta.com/externalKey
idpCert: classpath:octa.crt
privateKey: classpath:saml-private-key.key
spCert: classpath:saml-public-cert.crt
# Enterprise edition settings unused for now please ignore!
enterpriseEdition:
enabled: false # set to 'true' to enable enterprise edition
key: 00000000-0000-0000-0000-000000000000
CustomMetadata:
autoUpdateMetadata: false # set to 'true' to automatically update metadata with below values
author: username # Supports text such as 'John Doe' or types such as username to autopopulate with users username
autoUpdateMetadata: true # set to 'true' to automatically update metadata with below values
author: username # Supports text such as 'John Doe' or types such as username
creator: Stirling-PDF # Supports text such as 'Company-PDF'
producer: Stirling-PDF # Supports text such as 'Company-PDF'
legal:
termsAndConditions: https://www.stirlingpdf.com/terms-and-conditions # URL to the terms and conditions of your application (e.g. https://example.com/terms) Empty string to disable or filename to load from local file in static folder
privacyPolicy: https://www.stirlingpdf.com/privacy-policy # URL to the privacy policy of your application (e.g. https://example.com/privacy) Empty string to disable or filename to load from local file in static folder
termsAndConditions: '' # URL to the terms and conditions of your application (e.g. https://example.com/terms) Empty string to disable or filename to load from local file in static folder
privacyPolicy: '' # URL to the privacy policy of your application (e.g. https://example.com/privacy) Empty string to disable or filename to load from local file in static folder
accessibilityStatement: '' # URL to the accessibility statement of your application (e.g. https://example.com/accessibility) Empty string to disable or filename to load from local file in static folder
cookiePolicy: '' # URL to the cookie policy of your application (e.g. https://example.com/cookie) Empty string to disable or filename to load from local file in static folder
impressum: '' # URL to the impressum of your application (e.g. https://example.com/impressum) Empty string to disable or filename to load from local file in static folder

View File

@@ -21,13 +21,6 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "com.coveo:saml-client",
"moduleUrl": "https://github.com/coveo/saml-client",
"moduleVersion": "5.0.0",
"moduleLicense": "MIT",
"moduleLicenseUrl": "https://opensource.org/licenses/MIT"
},
{
"moduleName": "com.fasterxml.jackson.core:jackson-annotations",
"moduleUrl": "https://github.com/FasterXML/jackson",
@@ -70,27 +63,6 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.fasterxml.jackson.jaxrs:jackson-jaxrs-base",
"moduleUrl": "https://github.com/FasterXML/jackson-jaxrs-providers/jackson-jaxrs-base",
"moduleVersion": "2.17.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider",
"moduleUrl": "https://github.com/FasterXML/jackson-jaxrs-providers/jackson-jaxrs-json-provider",
"moduleVersion": "2.17.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.fasterxml.jackson.module:jackson-module-jaxb-annotations",
"moduleUrl": "https://github.com/FasterXML/jackson-modules-base",
"moduleVersion": "2.17.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.fasterxml.jackson.module:jackson-module-parameter-names",
"moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names",
@@ -139,46 +111,6 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.code.findbugs:jsr305",
"moduleUrl": "http://findbugs.sourceforge.net/",
"moduleVersion": "3.0.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.errorprone:error_prone_annotations",
"moduleVersion": "2.11.0",
"moduleLicense": "Apache 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.guava:failureaccess",
"moduleUrl": "https://github.com/google/guava/",
"moduleVersion": "1.0.1",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.guava:guava",
"moduleUrl": "https://github.com/google/guava/",
"moduleVersion": "31.1-jre",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.guava:listenablefuture",
"moduleVersion": "9999.0-empty-to-avoid-conflict-with-guava",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.j2objc:j2objc-annotations",
"moduleUrl": "https://github.com/google/j2objc/",
"moduleVersion": "1.3",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.google.zxing:core",
"moduleUrl": "https://github.com/zxing/zxing/core",
@@ -186,25 +118,6 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.googlecode.owasp-java-html-sanitizer:java10-shim",
"moduleVersion": "20240325.1",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.googlecode.owasp-java-html-sanitizer:java8-shim",
"moduleVersion": "20240325.1",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer",
"moduleUrl": "https://owasp.org",
"moduleVersion": "20240325.1",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.h2database:h2",
"moduleUrl": "https://h2database.com",
@@ -254,13 +167,6 @@
"moduleLicense": "Apache 2",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "com.posthog.java:posthog",
"moduleUrl": "http://github.com/PostHog/posthog-java",
"moduleVersion": "1.1.1",
"moduleLicense": "MIT License",
"moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php"
},
{
"moduleName": "com.sun.istack:istack-commons-runtime",
"moduleUrl": "https://www.eclipse.org",
@@ -270,78 +176,64 @@
},
{
"moduleName": "com.twelvemonkeys.common:common-image",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.common:common-io",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.common:common-lang",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-batik",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-bmp",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-core",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-jpeg",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-metadata",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-tiff",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.twelvemonkeys.imageio:imageio-webp",
"moduleVersion": "3.12.0",
"moduleVersion": "3.11.0",
"moduleLicense": "The BSD License",
"moduleLicenseUrl": "https://github.com/haraldk/TwelveMonkeys#license"
},
{
"moduleName": "com.unboundid.product.scim2:scim2-sdk-client",
"moduleUrl": "https://github.com/pingidentity/scim2",
"moduleVersion": "2.3.5",
"moduleLicense": "UnboundID SCIM2 SDK Free Use License",
"moduleLicenseUrl": "https://github.com/pingidentity/scim2"
},
{
"moduleName": "com.unboundid.product.scim2:scim2-sdk-common",
"moduleUrl": "https://github.com/pingidentity/scim2",
"moduleVersion": "2.3.5",
"moduleLicense": "UnboundID SCIM2 SDK Free Use License",
"moduleLicenseUrl": "https://github.com/pingidentity/scim2"
},
{
"moduleName": "com.zaxxer:HikariCP",
"moduleUrl": "https://github.com/brettwooldridge/HikariCP",
@@ -356,13 +248,6 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "commons-codec:commons-codec",
"moduleUrl": "https://commons.apache.org/proper/commons-codec/",
"moduleVersion": "1.16.1",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "commons-collections:commons-collections",
"moduleUrl": "http://commons.apache.org/collections/",
@@ -384,12 +269,6 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "/LICENSE.txt"
},
{
"moduleName": "io.dropwizard.metrics:metrics-core",
"moduleVersion": "4.2.25",
"moduleLicense": "Apache License 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.html"
},
{
"moduleName": "io.github.pixee:java-security-toolkit",
"moduleUrl": "https://github.com/pixee/java-security-toolkit",
@@ -407,7 +286,7 @@
{
"moduleName": "io.micrometer:micrometer-core",
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
"moduleVersion": "1.13.6",
"moduleVersion": "1.13.4",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
@@ -543,20 +422,6 @@
"moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception",
"moduleLicenseUrl": "https://www.gnu.org/software/classpath/license.html"
},
{
"moduleName": "javax.activation:javax.activation-api",
"moduleUrl": "http://www.oracle.com",
"moduleVersion": "1.2.0",
"moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0",
"moduleLicenseUrl": "https://opensource.org/licenses/CDDL-1.0"
},
{
"moduleName": "javax.xml.bind:jaxb-api",
"moduleUrl": "http://www.oracle.com/",
"moduleVersion": "2.3.1",
"moduleLicense": "GPL2 w/ CPE",
"moduleLicenseUrl": "https://oss.oracle.com/licenses/CDDL+GPL-1.1"
},
{
"moduleName": "net.bytebuddy:byte-buddy",
"moduleVersion": "1.14.19",
@@ -577,12 +442,6 @@
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "net.shibboleth.utilities:java-support",
"moduleVersion": "8.4.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.antlr:antlr4-runtime",
"moduleUrl": "https://www.antlr.org/",
@@ -611,20 +470,6 @@
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.httpcomponents:httpclient",
"moduleUrl": "http://hc.apache.org/httpcomponents-client-ga",
"moduleVersion": "4.5.14",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.httpcomponents:httpcore",
"moduleUrl": "http://hc.apache.org/httpcomponents-core-ga",
"moduleVersion": "4.4.16",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.logging.log4j:log4j-api",
"moduleVersion": "2.23.1",
@@ -671,13 +516,6 @@
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.santuario:xmlsec",
"moduleUrl": "https://www.apache.org/",
"moduleVersion": "2.3.4",
"moduleLicense": "Apache-2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.tomcat.embed:tomcat-embed-el",
"moduleUrl": "https://tomcat.apache.org/",
@@ -685,23 +523,16 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.velocity:velocity-engine-core",
"moduleUrl": "https://www.apache.org/",
"moduleVersion": "2.3",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.xmlgraphics:batik-all",
"moduleVersion": "1.18",
"moduleVersion": "1.17",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.apache.xmlgraphics:xmlgraphics-commons",
"moduleUrl": "http://xmlgraphics.apache.org/commons/",
"moduleVersion": "2.10",
"moduleVersion": "2.9",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
@@ -740,32 +571,18 @@
"moduleLicense": "Bouncy Castle Licence",
"moduleLicenseUrl": "https://www.bouncycastle.org/licence.html"
},
{
"moduleName": "org.checkerframework:checker-qual",
"moduleUrl": "https://checkerframework.org",
"moduleVersion": "3.12.0",
"moduleLicense": "The MIT License",
"moduleLicenseUrl": "http://opensource.org/licenses/MIT"
},
{
"moduleName": "org.commonmark:commonmark",
"moduleVersion": "0.24.0",
"moduleLicense": "BSD-2-Clause",
"moduleVersion": "0.23.0",
"moduleLicense": "BSD 2-Clause License",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},
{
"moduleName": "org.commonmark:commonmark-ext-gfm-tables",
"moduleVersion": "0.24.0",
"moduleLicense": "BSD-2-Clause",
"moduleVersion": "0.23.0",
"moduleLicense": "BSD 2-Clause License",
"moduleLicenseUrl": "https://opensource.org/licenses/BSD-2-Clause"
},
{
"moduleName": "org.cryptacular:cryptacular",
"moduleUrl": "http://www.cryptacular.org",
"moduleVersion": "1.2.5",
"moduleLicense": "GNU Lesser General Public License",
"moduleLicenseUrl": "http://www.gnu.org/licenses/lgpl-3.0.txt"
},
{
"moduleName": "org.eclipse.angus:angus-activation",
"moduleUrl": "https://www.eclipse.org",
@@ -1011,78 +828,6 @@
"moduleLicense": "Public Domain, per Creative Commons CC0",
"moduleLicenseUrl": "http://creativecommons.org/publicdomain/zero/1.0/"
},
{
"moduleName": "org.opensaml:opensaml-core",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-messaging-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-profile-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-saml-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-saml-impl",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-security-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-security-impl",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-soap-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-soap-impl",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-storage-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-xmlsec-api",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.opensaml:opensaml-xmlsec-impl",
"moduleVersion": "4.3.2",
"moduleLicense": "The Apache Software License, Version 2.0",
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
},
{
"moduleName": "org.ow2.asm:asm",
"moduleUrl": "http://asm.ow2.org",
@@ -1311,13 +1056,6 @@
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "org.springframework.security:spring-security-saml2-service-provider",
"moduleUrl": "https://spring.io/projects/spring-security",
"moduleVersion": "6.3.3",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},
{
"moduleName": "org.springframework.security:spring-security-web",
"moduleUrl": "https://spring.io/projects/spring-security",
@@ -1405,7 +1143,7 @@
{
"moduleName": "org.springframework:spring-webmvc",
"moduleUrl": "https://github.com/spring-projects/spring-framework",
"moduleVersion": "6.1.14",
"moduleVersion": "6.1.13",
"moduleLicense": "Apache License, Version 2.0",
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
},

View File

@@ -1,9 +1,7 @@
select#font-select,
select#font-select option {
height: 60px;
/* Adjust as needed */
font-size: 30px;
/* Adjust as needed */
height: 60px; /* Adjust as needed */
font-size: 30px; /* Adjust as needed */
}
.drawing-pad-container {
@@ -15,12 +13,10 @@ select#font-select option {
width: 100%;
height: 300px;
}
#box-drag-container {
position: relative;
margin: 20px 0;
}
.draggable-buttons-box {
position: absolute;
top: 0;
@@ -28,37 +24,16 @@ select#font-select option {
width: 100%;
display: flex;
gap: 5px;
z-index: 5;
}
.draggable-buttons-box>button {
z-index: 4;
.draggable-buttons-box > button {
z-index: 10;
background-color: rgba(13, 110, 253, 0.1);
}
.draggable-canvas {
border: 2px solid #3498db;
border: 1px solid red;
position: absolute;
touch-action: none;
user-select: none;
top: 0px;
left: 0;
z-index: 100;
cursor: grab;
transition: transform 0.1s ease-out;
background-color: rgba(52, 152, 219, 0.1);
/* Light blue background */
}
.draggable-canvas:active {
cursor: grabbing;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
/* Shadow on active drag */
}
.draggable-canvas:hover {
border: 2px solid #2980b9;
/* Darker border on hover */
background-color: rgba(52, 152, 219, 0.2);
/* Darken background on hover */
}

View File

@@ -1,36 +0,0 @@
const draggableElement = document.querySelector('.draggable-canvas');
// Variables to store the current position of the draggable element
let offsetX, offsetY, isDragging = false;
draggableElement.addEventListener('mousedown', (e) => {
// Get the offset when the mouse is clicked inside the element
offsetX = e.clientX - draggableElement.getBoundingClientRect().left;
offsetY = e.clientY - draggableElement.getBoundingClientRect().top;
// Set isDragging to true
isDragging = true;
// Add event listeners for mouse movement and release
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (isDragging) {
// Calculate the new position of the element
const left = e.clientX - offsetX;
const top = e.clientY - offsetY;
// Move the element by setting its style
draggableElement.style.left = `${left}px`;
draggableElement.style.top = `${top}px`;
}
}
function onMouseUp() {
// Stop dragging and remove event listeners
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}

View File

@@ -0,0 +1,39 @@
const scrollDivHorizontally = (id) => {
var scrollDeltaX = 0; // variable to store the accumulated horizontal scroll delta
var scrollDeltaY = 0; // variable to store the accumulated vertical scroll delta
var isScrolling = false; // variable to track if scroll is already in progress
const divToScroll = document.getElementById(id);
function scrollLoop() {
// Scroll the div horizontally and vertically by a fraction of the accumulated scroll delta
divToScroll.scrollLeft += scrollDeltaX * 0.1;
divToScroll.scrollTop += scrollDeltaY * 0.1;
// Reduce the accumulated scroll delta by a fraction
scrollDeltaX *= 0.9;
scrollDeltaY *= 0.9;
// If scroll delta is still significant, continue the scroll loop
if (Math.abs(scrollDeltaX) > 0.1 || Math.abs(scrollDeltaY) > 0.1) {
requestAnimationFrame(scrollLoop);
} else {
isScrolling = false; // Reset scroll in progress flag
}
}
divToScroll.addEventListener("wheel", function (e) {
e.preventDefault(); // prevent default mousewheel behavior
// Accumulate the horizontal and vertical scroll delta
scrollDeltaX -= e.deltaX || e.wheelDeltaX || -e.deltaY || -e.wheelDeltaY;
scrollDeltaY -= e.deltaY || e.wheelDeltaY || -e.deltaX || -e.wheelDeltaX;
// If scroll is not already in progress, start the scroll loop
if (!isScrolling) {
isScrolling = true;
requestAnimationFrame(scrollLoop);
}
});
};
export default scrollDivHorizontally;

View File

@@ -283,7 +283,7 @@
</script>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<div th:if="${altLogin}" class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div th:if="${oAuth2Enabled}" class="modal fade" id="editUserModal" tabindex="-1" role="dialog" aria-labelledby="editUserModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">

View File

@@ -360,7 +360,7 @@
</div>
</li>
<li class="nav-item" th:if="${!@runningEE}">
<li class="nav-item">
<a href="https://stirlingpdf.com/pricing" class="nav-link go-pro-link" target="_blank" rel="noopener noreferrer">
<span class="go-pro-badge" th:text="#{enterpriseEdition.button}"></span>
</a>

View File

@@ -374,10 +374,10 @@
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow us to understand our users better.</p>
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file</p>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}" onclick="setAnalytics(true)">Enable analytics</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)" th:text="#{analytics.disable}">Disable analytics</button>
</div>
</div>
</div>
</div>
@@ -426,60 +426,50 @@
document.addEventListener("DOMContentLoaded", function() {
const surveyVersion = "2.0";
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
const dontShowAgain = document.getElementById('dontShowAgain');
const takeSurveyButton = document.getElementById('takeSurvey');
document.addEventListener("DOMContentLoaded", function() {
const surveyVersion = "2.0";
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
const dontShowAgain = document.getElementById('dontShowAgain');
const takeSurveyButton = document.getElementById('takeSurvey');
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
const viewThresholds = [5, 15, 30, 50, 75, 100, 150, 200];
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
// Check if survey version changed and reset page views if it did
const storedVersion = localStorage.getItem('surveyVersion');
if (storedVersion && storedVersion !== surveyVersion) {
localStorage.setItem('pageViews', '0');
}
pageViews++;
localStorage.setItem('pageViews', pageViews.toString());
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
pageViews++;
localStorage.setItem('pageViews', pageViews.toString());
function shouldShowSurvey() {
if (localStorage.getItem('dontShowSurvey') === 'true' ||
localStorage.getItem('surveyTaken') === 'true') {
return false;
function shouldShowSurvey() {
if (localStorage.getItem('dontShowSurvey') === 'true' || localStorage.getItem('surveyTaken') === 'true') {
return false;
}
// If survey version changed and we hit a threshold, show the survey
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
viewThresholds.includes(pageViews)) {
return true;
if (localStorage.getItem('surveyVersion') !== surveyVersion) {
return true;
}
return viewThresholds.includes(pageViews);
}
}
if (shouldShowSurvey()) {
if (shouldShowSurvey()) {
modal.show();
}
}
dontShowAgain.addEventListener('change', function() {
dontShowAgain.addEventListener('change', function() {
if (this.checked) {
localStorage.setItem('dontShowSurvey', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
localStorage.setItem('dontShowSurvey', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
} else {
localStorage.removeItem('dontShowSurvey');
localStorage.removeItem('surveyVersion');
localStorage.removeItem('dontShowSurvey');
localStorage.removeItem('surveyVersion');
}
});
});
takeSurveyButton.addEventListener('click', function() {
takeSurveyButton.addEventListener('click', function() {
localStorage.setItem('surveyTaken', 'true');
localStorage.setItem('surveyVersion', surveyVersion);
modal.hide();
});
});
});
</script>

View File

@@ -114,7 +114,7 @@
<img class="my-4" th:src="@{'/favicon.svg'}" alt="favicon" width="144" height="144">
<h1 class="h1 mb-3 fw-normal" th:text="${@appName}">Stirling-PDF</h1>
<div th:if="${altLogin} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')">
<div th:if="${oAuth2Enabled} and (${loginMethod} == 'all' or ${loginMethod} == 'oauth2')">
<a href="#" class="w-100 btn btn-lg btn-primary" data-bs-toggle="modal" data-bs-target="#loginsModal" th:text="#{login.ssoSignIn}">Login Via SSO</a>
<br>
<br>
@@ -168,7 +168,7 @@
</main>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<div th:if="${altLogin}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
<div th:if="${oAuth2Enabled}" class="modal fade" id="loginsModal" tabindex="-1" role="dialog" aria-labelledby="loginsModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
@@ -181,7 +181,7 @@
</div>
<div class="modal-body">
<div class="mb-3" th:each="provider : ${providerlist}">
<a th:href="@{|${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">Login Provider</a>
<a th:href="@{|/oauth2/authorization/${provider.key}|}" th:text="${provider.value}" class="w-100 btn btn-lg btn-primary">OpenID Connect</a>
</div>
</div>
<div class="modal-footer">

View File

@@ -80,6 +80,7 @@
<script type="module">
import PdfContainer from './js/multitool/PdfContainer.js';
import DragDropManager from "./js/multitool/DragDropManager.js";
import scrollDivHorizontally from "./js/multitool/horizontalScroll.js";
import ImageHighlighter from "./js/multitool/ImageHighlighter.js";
import PdfActionsManager from './js/multitool/PdfActionsManager.js';
import FileDragManager from './js/multitool/fileInput.js';
@@ -92,6 +93,7 @@
const fileDragManager = new FileDragManager();
// Scroll the wrapper horizontally
scrollDivHorizontally('pages-container-wrapper');
// Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons.
const pdfContainer = new PdfContainer(
@@ -109,4 +111,4 @@
</script>
</body>
</html>
</html>

View File

@@ -1,11 +1,8 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{sign.title}, header=#{sign.header})}"></th:block>
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
<link rel="stylesheet" th:href="@{'/css/sign.css'}">
<th:block th:each="font : ${fonts}">
<style th:inline="text">
@font-face {
@@ -14,266 +11,268 @@
}
#font-select option[value="[[${font.name}]]"] {
font-family: "[[${font.name}]]",
cursive;
font-family: "[[${font.name}]]", cursive;
}
#font-select option[value='/*[[${font.name}]]*/'] {
font-family: '/*[[${font.name}]]*/', cursive;
}
</style>
</th:block>
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
</head>
<script th:src="@{'/js/thirdParty/signature_pad.umd.min.js'}"></script>
<script th:src="@{'/js/thirdParty/interact.min.js'}"></script>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
<span class="tool-header-text" th:text="#{sign.header}"></span>
</div>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-6 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon sign">signature</span>
<span class="tool-header-text" th:text="#{sign.header}"></span>
</div>
<!-- pdf selector -->
<div th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}"></div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
<!-- pdf selector -->
<div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, disableMultipleFiles=true, accept='application/pdf')}">
</div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script>
let originalFileName = '';
document.querySelector('input[name=pdf-upload]').addEventListener('change', async (event) => {
const file = event.target.files[0];
if (file) {
originalFileName = file.name.replace(/\.[^/.]+$/, "");
const pdfData = await file.arrayBuffer();
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
const pdfDoc = await pdfjsLib.getDocument({ data: pdfData }).promise;
await DraggableUtils.renderPage(pdfDoc, 0);
document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = '';
});
}
});
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = "display:none !important";
document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = '';
})
}
});
});
</script>
<div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{sign.upload}">
<div
th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}">
</div>
<script>
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => {
if (!e.target.files) return;
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});
</script>
</div>
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
<canvas id="drawing-pad-canvas"></canvas>
<br>
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()"
th:text="#{sign.clear}"></button>
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()"
th:text="#{sign.add}"></button>
<script>
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: 'black',
});
function addDraggableFromPad() {
if (signaturePad.isEmpty()) return;
const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas);
console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
}
function getCroppedCanvasDataUrl(canvas) {
let originalCtx = canvas.getContext('2d');
let originalWidth = canvas.width;
let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0, 0, originalWidth, originalHeight);
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
for (y = 0; y < originalHeight; y++) {
for (x = 0; x < originalWidth; x++) {
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x;
if (maxX < x) maxX = x;
if (minY > y) minY = y;
if (maxY < y) maxY = y;
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".show-on-file-selected").forEach(el => {
el.style.cssText = "display:none !important";
})
});
</script>
<div class="tab-group show-on-file-selected">
<div class="tab-container" th:title="#{sign.upload}">
<div th:replace="~{fragments/common :: fileSelector(name='image-upload', multipleInputsForSingleRequest=true, accept='image/*', inputText=#{imgPrompt})}"></div>
<script>
const imageUpload = document.querySelector('input[name=image-upload]');
imageUpload.addEventListener('change', e => {
if(!e.target.files) {
return;
}
for (const imageFile of e.target.files) {
var reader = new FileReader();
reader.readAsDataURL(imageFile);
reader.onloadend = function (e) {
DraggableUtils.createDraggableCanvasFromUrl(e.target.result);
};
}
});
</script>
</div>
<div class="tab-container drawing-pad-container" th:title="#{sign.draw}">
<canvas id="drawing-pad-canvas"></canvas>
<br>
<button id="clear-signature" class="btn btn-outline-danger mt-2" onclick="signaturePad.clear()" th:text="#{sign.clear}"></button>
<button id="save-signature" class="btn btn-outline-success mt-2" onclick="addDraggableFromPad()" th:text="#{sign.add}"></button>
<script>
const signaturePadCanvas = document.getElementById('drawing-pad-canvas');
const signaturePad = new SignaturePad(signaturePadCanvas, {
minWidth: 1,
maxWidth: 2,
penColor: 'black',
});
function addDraggableFromPad() {
if (signaturePad.isEmpty()) return;
const startTime = Date.now();
const croppedDataUrl = getCroppedCanvasDataUrl(signaturePadCanvas)
console.log(Date.now() - startTime);
DraggableUtils.createDraggableCanvasFromUrl(croppedDataUrl);
}
}
function getCroppedCanvasDataUrl(canvas) {
// code is from: https://github.com/szimek/signature_pad/issues/49#issuecomment-1104035775
let originalCtx = canvas.getContext('2d');
let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null;
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
let originalWidth = canvas.width;
let originalHeight = canvas.height;
let imageData = originalCtx.getImageData(0,0, originalWidth, originalHeight);
let croppedCanvas = document.createElement('canvas'),
croppedCtx = croppedCanvas.getContext('2d');
let minX = originalWidth + 1, maxX = -1, minY = originalHeight + 1, maxY = -1, x = 0, y = 0, currentPixelColorValueIndex;
croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight;
croppedCtx.putImageData(cuttedImageData, 0, 0);
for (y = 0; y < originalHeight; y++) {
for (x = 0; x < originalWidth; x++) {
currentPixelColorValueIndex = (y * originalWidth + x) * 4;
let currentPixelAlphaValue = imageData.data[currentPixelColorValueIndex + 3];
if (currentPixelAlphaValue > 0) {
if (minX > x) minX = x;
if (maxX < x) maxX = x;
if (minY > y) minY = y;
if (maxY < y) maxY = y;
}
}
}
return croppedCanvas.toDataURL();
}
let croppedWidth = maxX - minX;
let croppedHeight = maxY - minY;
if (croppedWidth < 0 || croppedHeight < 0) return null;
let cuttedImageData = originalCtx.getImageData(minX, minY, croppedWidth, croppedHeight);
function resizeCanvas() {
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10;
let croppedCanvas = document.createElement('canvas'),
croppedCtx = croppedCanvas.getContext('2d');
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
croppedCanvas.width = croppedWidth;
croppedCanvas.height = croppedHeight;
croppedCtx.putImageData(cuttedImageData, 0, 0);
signaturePad.clear();
}
return croppedCanvas.toDataURL();
}
function resizeCanvas() {
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
var ratio = Math.max(window.devicePixelRatio || 1, 1);
var additionalFactor = 10;
new IntersectionObserver((entries, observer) => {
if (entries.some(entry => entry.intersectionRatio > 0)) {
resizeCanvas();
}
}).observe(signaturePadCanvas);
signaturePadCanvas.width = signaturePadCanvas.offsetWidth * ratio * additionalFactor;
signaturePadCanvas.height = signaturePadCanvas.offsetHeight * ratio * additionalFactor;
signaturePadCanvas.getContext("2d").scale(ratio * additionalFactor, ratio * additionalFactor);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
</script>
</div>
// This library does not listen for canvas changes, so after the canvas is automatically
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
// that the state of this library is consistent with visual state of the canvas, you
// have to clear it manually.
signaturePad.clear();
}
new IntersectionObserver((entries, observer) => {
if (entries.some(entry => entry.intersectionRatio > 0)) {
resizeCanvas();
}
}).observe(signaturePadCanvas);
new ResizeObserver(resizeCanvas).observe(signaturePadCanvas);
</script>
</div>
<div class="tab-container" th:title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}" th:class="${font.name.toLowerCase()+'-font'}">
</option>
</select>
<div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center" onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
</div>
<script>
function addDraggableFromText() {
const sigText = document.getElementById('sigText').value;
const font = document.querySelector('select[name=font]').value;
const fontSize = 100;
<div class="tab-container" th:title="#{sign.text}">
<label class="form-check-label" for="sigText" th:text="#{text}"></label>
<textarea class="form-control" id="sigText" name="sigText" rows="3"></textarea>
<label th:text="#{font}"></label>
<select class="form-control" name="font" id="font-select">
<option th:each="font : ${fonts}" th:value="${font.name}" th:text="${font.name}"
th:class="${font.name.toLowerCase()+'-font'}"></option>
</select>
<div class="margin-auto-parent">
<button id="save-text-signature" class="btn btn-outline-success mt-2 margin-center"
onclick="addDraggableFromText()" th:text="#{sign.add}"></button>
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize;
let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight*1.35; //for tails
ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top';
let y = 0;
paragraphs.forEach(paragraph => {
ctx.fillText(paragraph, 0, y);
y += fontSize;
});
const dataURL = canvas.toDataURL();
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
}
</script>
<script>
const sigTextInput = document.getElementById('sigText');
const fontSelect = document.getElementById('font-select');
const updateOptionTexts = () => {
Array.from(fontSelect.options).forEach(option => {
const fontName = option.value.replace(/-regular$/i, '');
option.text = sigTextInput.value || fontName;
});
}
sigTextInput.addEventListener('input', updateOptionTexts);
fontSelect.addEventListener('change', (e) => {
e.target.style.fontFamily = e.target.value;
updateOptionTexts();
});
// Manually trigger the change event
fontSelect.dispatchEvent(new Event('change'));
</script>
</div>
<script>
function addDraggableFromText() {
const sigText = document.getElementById('sigText').value;
const font = document.querySelector('select[name=font]').value;
const fontSize = 100;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `${fontSize}px ${font}`;
const textWidth = ctx.measureText(sigText).width;
const textHeight = fontSize;
let paragraphs = sigText.split(/\r?\n/);
canvas.width = textWidth;
canvas.height = paragraphs.length * textHeight * 1.35; // for tails
ctx.font = `${fontSize}px ${font}`;
ctx.textBaseline = 'top';
let y = 0;
paragraphs.forEach(paragraph => {
ctx.fillText(paragraph, 0, y);
y += fontSize;
});
const dataURL = canvas.toDataURL();
DraggableUtils.createDraggableCanvasFromUrl(dataURL);
}
</script>
</div>
</div>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected">
<canvas id="pdf-canvas"></canvas>
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script th:src="@{'/js/draggable-utils.js'}"></script>
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary"
onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash"
viewBox="0 0 16 16">
<path
d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z" />
<path
d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z" />
</svg>
</button>
<button class="btn btn-outline-secondary"
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()"
style="margin-left:auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
</svg>
</button>
<button class="btn btn-outline-secondary"
onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</svg>
</button>
<!-- draggables box -->
<div id="box-drag-container" class="show-on-file-selected">
<canvas id="pdf-canvas"></canvas>
<script th:src="@{'/js/thirdParty/pdf-lib.min.js'}"></script>
<script th:src="@{'/js/draggable-utils.js'}"></script>
<div class="draggable-buttons-box ignore-rtl">
<button class="btn btn-outline-secondary" onclick="DraggableUtils.deleteDraggableCanvas(DraggableUtils.getLastInteracted())">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/>
<path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1ZM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118ZM2.5 3h11V2h-11v1Z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.incrementPage() : DraggableUtils.decrementPage()" style="margin-left:auto">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
</svg>
</button>
<button class="btn btn-outline-secondary" onclick="document.documentElement.getAttribute('dir')==='rtl' ? DraggableUtils.decrementPage() : DraggableUtils.incrementPage()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- download button -->
<div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center"
th:text="#{downloadPdf}"></button>
</div>
<!-- download button -->
<div class="margin-auto-parent">
<button id="download-pdf" class="btn btn-primary mb-2 show-on-file-selected margin-center" th:text="#{downloadPdf}"></button>
</div>
<script>
document.getElementById("download-pdf").addEventListener('click', async () => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_signed.pdf';
link.click();
});
</script>
<script>
document.getElementById("download-pdf").addEventListener('click', async() => {
const modifiedPdf = await DraggableUtils.getOverlayedPdfDocument();
const modifiedPdfBytes = await modifiedPdf.save();
const blob = new Blob([modifiedPdfBytes], { type: 'application/pdf' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = originalFileName + '_signed.pdf';
link.click();
});
</script>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
<!-- Link the draggable.js file -->
<script src="/path/to/your/draggable.js"></script>
</body>
</body>
</html>