Compare commits
31 Commits
v0.30.1
...
Frooodle/l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8709c25c0 | ||
|
|
261e69f3ca | ||
|
|
addb999301 | ||
|
|
496fcad698 | ||
|
|
daf4f49050 | ||
|
|
2cdad2f9ff | ||
|
|
4c4613cfec | ||
|
|
8aa0e85c20 | ||
|
|
1129487aaf | ||
|
|
378d0f8afb | ||
|
|
a12de218c0 | ||
|
|
1ba271b0f4 | ||
|
|
6ccc29d0a7 | ||
|
|
eee2f5c666 | ||
|
|
e660237e28 | ||
|
|
83e93688ee | ||
|
|
dedfabd630 | ||
|
|
3b5b7772a9 | ||
|
|
c59d3ff3e0 | ||
|
|
5832147b30 | ||
|
|
20dc2f60cd | ||
|
|
b46ccdde44 | ||
|
|
4fa1b4adb0 | ||
|
|
03158b05e4 | ||
|
|
28c55ca80c | ||
|
|
6ca14edaf1 | ||
|
|
f9677b1fe8 | ||
|
|
04a6ebf515 | ||
|
|
262e2ed47a | ||
|
|
4436759e12 | ||
|
|
87925ac618 |
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
64
README.md
64
README.md
@@ -172,42 +172,42 @@ Stirling PDF currently supports 38!
|
||||
|
||||
| Language | Progress |
|
||||
| ------------------------------------------- | -------------------------------------- |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| Arabic (العربية) (ar_AR) |  |
|
||||
| Basque (Euskara) (eu_ES) |  |
|
||||
| Bulgarian (Български) (bg_BG) |  |
|
||||
| Catalan (Català) (ca_CA) |  |
|
||||
| Croatian (Hrvatski) (hr_HR) |  |
|
||||
| Czech (Česky) (cs_CZ) |  |
|
||||
| Danish (Dansk) (da_DK) |  |
|
||||
| Dutch (Nederlands) (nl_NL) |  |
|
||||
| English (English) (en_GB) |  |
|
||||
| English (US) (en_US) |  |
|
||||
| French (Français) (fr_FR) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| German (Deutsch) (de_DE) |  |
|
||||
| Greek (Ελληνικά) (el_GR) |  |
|
||||
| Hindi (हिंदी) (hi_IN) |  |
|
||||
| Hungarian (Magyar) (hu_HU) |  |
|
||||
| Indonesia (Bahasa Indonesia) (id_ID) |  |
|
||||
| Irish (Gaeilge) (ga_IE) |  |
|
||||
| Italian (Italiano) (it_IT) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
| Japanese (日本語) (ja_JP) |  |
|
||||
| Korean (한국어) (ko_KR) |  |
|
||||
| Norwegian (Norsk) (no_NB) |  |
|
||||
| Polish (Polski) (pl_PL) |  |
|
||||
| Portuguese (Português) (pt_PT) |  |
|
||||
| Portuguese Brazilian (Português) (pt_BR) |  |
|
||||
| Romanian (Română) (ro_RO) |  |
|
||||
| Russian (Русский) (ru_RU) |  |
|
||||
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) |  |
|
||||
| Simplified Chinese (简体中文) (zh_CN) |  |
|
||||
| Slovakian (Slovensky) (sk_SK) |  |
|
||||
| Spanish (Español) (es_ES) |  |
|
||||
| Swedish (Svenska) (sv_SE) |  |
|
||||
| Thai (ไทย) (th_TH) |  |
|
||||
| Traditional Chinese (繁體中文) (zh_TW) |  |
|
||||
| Turkish (Türkçe) (tr_TR) |  |
|
||||
| Ukrainian (Українська) (uk_UA) |  |
|
||||
| Vietnamese (Tiếng Việt) (vi_VN) |  |
|
||||
|
||||
## Contributing (creating issues, translations, fixing bugs, etc.)
|
||||
|
||||
|
||||
30
build.gradle
30
build.gradle
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 ]", "");
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 "/";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package stirling.software.SPDF.config.security.saml;
|
||||
|
||||
public interface Saml2AuthorityAttributeLookup {
|
||||
String getAuthorityAttribute(String registrationId);
|
||||
|
||||
SimpleScimMappings getIdentityMappings(String registrationId);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package stirling.software.SPDF.config.security.saml;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SimpleScimMappings {
|
||||
String givenName;
|
||||
String familyName;
|
||||
String email;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -4,6 +4,4 @@ public interface UserServiceInterface {
|
||||
String getApiKeyForUser(String username);
|
||||
|
||||
String getCurrentUsername();
|
||||
|
||||
long getTotalUsersCount();
|
||||
}
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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=حجب تلقائي
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=Αυτόματο Μαύρισμα Κειμένου
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 l’utilisateur à changer son nom d’util
|
||||
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 n’importe quel fichier en PDF (DOCX, PNG, XLS, PPT, TXT, etc.).
|
||||
home.fileToPDF.desc=Convertissez presque n’importe 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 d’un 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=स्वत: गोपनीयकरण
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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=自動塗りつぶし
|
||||
|
||||
@@ -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=자동 검열
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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ă
|
||||
|
||||
@@ -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=Автоматическое редактирование
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=ซ่อนข้อมูลอัตโนมัติ
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=Автоматичне редагування
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=自动删除
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
39
src/main/resources/static/js/multitool/horizontalScroll.js
Normal file
39
src/main/resources/static/js/multitool/horizontalScroll.js
Normal 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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user