Ensure Pixel gets disabled, PDF ToC support (#3659)

# Description of Changes

Please provide a summary of the changes, including:

- What was changed
- Why the change was made
- Any challenges encountered

Closes #(issue_number)

---

## Checklist

### General

- [ ] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have read the [Stirling-PDF Developer
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md)
(if applicable)
- [ ] I have read the [How to add new languages to
Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md)
(if applicable)
- [ ] I have performed a self-review of my own code
- [ ] My changes generate no new warnings

### Documentation

- [ ] I have updated relevant docs on [Stirling-PDF's doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
(if functionality has heavily changed)
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

### UI Changes (if applicable)

- [ ] Screenshots or videos demonstrating the UI changes are attached
(e.g., as comments or direct attachments in the PR)

### Testing (if applicable)

- [ ] I have tested my changes locally. Refer to the [Testing
Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing)
for more details.

---------

Co-authored-by: Dario Ghunney Ware <dariogware@gmail.com>
Co-authored-by: Connor Yoh <con.yoh13@gmail.com>
Co-authored-by: a <a>
Co-authored-by: Reece <reecebrowne1995@gmail.com>
This commit is contained in:
Anthony Stirling 2025-06-11 17:21:37 +01:00 committed by GitHub
parent bdc35519da
commit 1f2365f03c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
126 changed files with 39003 additions and 26979 deletions

View File

@ -47,18 +47,56 @@ jobs:
env: env:
DISABLE_ADDITIONAL_FEATURES: false DISABLE_ADDITIONAL_FEATURES: false
- name: Upload Test Reports - name: Check Test Reports Exist
id: check-reports
if: always() if: always()
run: |
missing_reports=()
# Check for required test report directories
if [ ! -d "stirling-pdf/build/reports/tests/" ]; then
missing_reports+=("stirling-pdf/build/reports/tests/")
fi
if [ ! -d "stirling-pdf/build/test-results/" ]; then
missing_reports+=("stirling-pdf/build/test-results/")
fi
if [ ! -d "common/build/reports/tests/" ]; then
missing_reports+=("common/build/reports/tests/")
fi
if [ ! -d "common/build/test-results/" ]; then
missing_reports+=("common/build/test-results/")
fi
if [ ! -d "proprietary/build/reports/tests/" ]; then
missing_reports+=("proprietary/build/reports/tests/")
fi
if [ ! -d "proprietary/build/test-results/" ]; then
missing_reports+=("proprietary/build/test-results/")
fi
# Fail if any required reports are missing
if [ ${#missing_reports[@]} -gt 0 ]; then
echo "ERROR: The following required test report directories are missing:"
printf '%s\n' "${missing_reports[@]}"
exit 1
fi
echo "All required test report directories are present"
- name: Upload Test Reports
if: steps.check-reports.outcome == 'success'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: test-reports-jdk-${{ matrix.jdk-version }} name: test-reports-jdk-${{ matrix.jdk-version }}
path: | path: |
build/reports/tests/ stirling-pdf/build/reports/tests/
build/test-results/ stirling-pdf/build/test-results/
build/reports/problems/ stirling-pdf/build/reports/problems/
/common/build/reports/tests/ common/build/reports/tests/
/common/build/test-results/ common/build/test-results/
/common/build/reports/problems/ common/build/reports/problems/
proprietary/build/reports/tests/
proprietary/build/test-results/
proprietary/build/reports/problems/
retention-days: 3 retention-days: 3
check-licence: check-licence:

View File

@ -5,8 +5,7 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
COPY scripts /scripts COPY scripts /scripts
COPY pipeline /pipeline COPY pipeline /pipeline
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY stirling-pdf/build/libs/*.jar app.jar
COPY build/libs/*.jar app.jar
ARG VERSION_TAG ARG VERSION_TAG

View File

@ -5,6 +5,7 @@ COPY build.gradle .
COPY settings.gradle . COPY settings.gradle .
COPY gradlew . COPY gradlew .
COPY gradle gradle/ COPY gradle gradle/
COPY stirling-pdf/build.gradle stirling-pdf/.
COPY common/build.gradle common/. COPY common/build.gradle common/.
COPY proprietary/build.gradle proprietary/. COPY proprietary/build.gradle proprietary/.
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0 RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
@ -27,7 +28,7 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
COPY scripts /scripts COPY scripts /scripts
COPY pipeline /pipeline COPY pipeline /pipeline
COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY stirling-pdf/src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY --from=build /app/build/libs/*.jar app.jar COPY --from=build /app/stirling-pdf/build/libs/*.jar app.jar
ARG VERSION_TAG ARG VERSION_TAG

View File

@ -18,7 +18,7 @@ COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY scripts/installFonts.sh /scripts/installFonts.sh COPY scripts/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline COPY pipeline /pipeline
COPY build/libs/*.jar app.jar COPY stirling-pdf/build/libs/*.jar app.jar
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \

View File

@ -1,8 +1,8 @@
plugins { plugins {
id "java" id "java"
id "jacoco" id "jacoco"
id "org.springframework.boot" version "3.5.0"
id "io.spring.dependency-management" version "1.1.7" id "io.spring.dependency-management" version "1.1.7"
id "org.springframework.boot" version "3.5.0"
id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.6" id "edu.sc.seis.launch4j" version "3.0.6"
@ -50,7 +50,6 @@ sourceSets {
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) { && System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
exclude 'stirling/software/proprietary/security/**' exclude 'stirling/software/proprietary/security/**'
} }
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') { if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
exclude 'stirling/software/SPDF/UI/impl/**' exclude 'stirling/software/SPDF/UI/impl/**'
} }
@ -75,7 +74,7 @@ sourceSets {
allprojects { allprojects {
group = 'stirling.software' group = 'stirling.software'
version = '0.46.2' version = '1.0.0'
configurations.configureEach { configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
@ -130,6 +129,7 @@ subprojects {
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {
@ -146,6 +146,11 @@ subprojects {
} }
} }
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
dependsOn "spotlessApply"
}
licenseReport { licenseReport {
renderers = [new JsonReportRenderer()] renderers = [new JsonReportRenderer()]
allowedLicensesFile = new File("$projectDir/allowed-licenses.json") allowedLicensesFile = new File("$projectDir/allowed-licenses.json")
@ -468,6 +473,7 @@ spotless {
target sourceSets.main.allJava target sourceSets.main.allJava
target project(':common').sourceSets.main.allJava target project(':common').sourceSets.main.allJava
target project(':proprietary').sourceSets.main.allJava target project(':proprietary').sourceSets.main.allJava
target project(':stirling-pdf').sourceSets.main.allJava
googleJavaFormat("1.27.0").aosp().reorderImports(false) googleJavaFormat("1.27.0").aosp().reorderImports(false)
@ -500,12 +506,17 @@ swaggerhubUpload {
oas = "3.0.0" // The version of the OpenAPI Specification you"re using oas = "3.0.0" // The version of the OpenAPI Specification you"re using
} }
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
}
tasks.named("test") { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.register('writeVersion') { tasks.register('writeVersion') {
def propsFile = file("$projectDir/stirling-pdf/src/main/resources/version.properties") def propsFile = file("$projectDir/common/src/main/resources/version.properties")
def propsDir = propsFile.parentFile def propsDir = propsFile.parentFile
doLast { doLast {
@ -529,6 +540,7 @@ tasks.register('writeVersion') {
} }
processResources.dependsOn(writeVersion) processResources.dependsOn(writeVersion)
project(':stirling-pdf').tasks.bootJar.dependsOn(writeVersion)
tasks.register('printVersion') { tasks.register('printVersion') {
doLast { doLast {
@ -545,3 +557,22 @@ tasks.register('printMacVersion') {
tasks.named('generateOpenApiDocs') { tasks.named('generateOpenApiDocs') {
doNotTrackState("Tracking state is not supported for this task") doNotTrackState("Tracking state is not supported for this task")
} }
tasks.named('bootRun') {
group = 'application'
description = 'Delegates to :stirling-pdf:bootRun'
dependsOn ':stirling-pdf:bootRun'
doFirst {
println "Delegating to :stirling-pdf:bootRun"
}
}
tasks.named('build') {
group = 'build'
description = 'Delegates to :stirling-pdf:bootJar'
dependsOn ':stirling-pdf:bootJar'
doFirst {
println "Delegating to :stirling-pdf:bootJar"
}
}

View File

@ -1,3 +1,8 @@
// Configure bootRun to disable it or point to a main class
bootRun {
enabled = false
}
dependencies { dependencies {
api 'org.springframework.boot:spring-boot-starter-web' api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-thymeleaf' api 'org.springframework.boot:spring-boot-starter-thymeleaf'
@ -12,4 +17,4 @@ dependencies {
api 'org.snakeyaml:snakeyaml-engine:2.9' api 'org.snakeyaml:snakeyaml-engine:2.9'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8" api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8"
api 'jakarta.mail:jakarta.mail-api:2.1.3' api 'jakarta.mail:jakarta.mail-api:2.1.3'
} }

View File

@ -1,7 +1,5 @@
package stirling.software.common.configuration; package stirling.software.common.configuration;
import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -10,25 +8,22 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Properties; import java.util.Properties;
import java.util.function.Predicate; import java.util.function.Predicate;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
@Lazy @Lazy
@ -253,9 +248,35 @@ public class AppConfig {
return applicationProperties.getSystem().getDatasource(); return applicationProperties.getSystem().getDatasource();
} }
@Bean(name = "runningProOrHigher")
@Profile("default")
public boolean runningProOrHigher() {
return false;
}
@Bean(name = "runningEE")
@Profile("default")
public boolean runningEnterprise() {
return false;
}
@Bean(name = "GoogleDriveEnabled")
@Profile("default")
public boolean googleDriveEnabled() {
return false;
}
@Bean(name = "license")
@Profile("default")
public String licenseType() {
return "NORMAL";
}
@Bean(name = "disablePixel") @Bean(name = "disablePixel")
public boolean disablePixel() { public boolean disablePixel() {
return Boolean.getBoolean(env.getProperty("DISABLE_PIXEL")); return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
} }
@Bean(name = "machineType") @Bean(name = "machineType")

View File

@ -1,7 +1,9 @@
repositories { repositories {
maven { url = "https://build.shibboleth.net/maven/releases" } maven { url = "https://build.shibboleth.net/maven/releases" }
} }
bootRun {
enabled = false
}
dependencies { dependencies {
implementation project(':common') implementation project(':common')

View File

@ -0,0 +1,44 @@
package stirling.software.proprietary.model;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.*;
import lombok.*;
import stirling.software.proprietary.security.model.User;
@Entity
@Table(name = "teams")
@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(onlyExplicitlyIncluded = true)
public class Team implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "team_id")
private Long id;
@Column(name = "name", unique = true, nullable = false)
private String name;
@OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<User> users = new HashSet<>();
public void addUser(User user) {
users.add(user);
user.setTeam(this);
}
public void removeUser(User user) {
users.remove(user);
user.setTeam(null);
}
}

View File

@ -0,0 +1,20 @@
package stirling.software.proprietary.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class TeamWithUserCountDTO {
private Long id;
private String name;
private Long userCount;
// Constructor for JPQL projection
public TeamWithUserCountDTO(Long id, String name, Long userCount) {
this.id = id;
this.name = name;
this.userCount = userCount;
}
}

View File

@ -1,6 +1,7 @@
package stirling.software.proprietary.security; package stirling.software.proprietary.security;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@ -13,7 +14,10 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role; import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.service.DatabaseServiceInterface; import stirling.software.proprietary.security.service.DatabaseServiceInterface;
import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
@Slf4j @Slf4j
@ -22,9 +26,8 @@ import stirling.software.proprietary.security.service.UserService;
public class InitialSecuritySetup { public class InitialSecuritySetup {
private final UserService userService; private final UserService userService;
private final TeamService teamService;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final DatabaseServiceInterface databaseService; private final DatabaseServiceInterface databaseService;
@PostConstruct @PostConstruct
@ -40,6 +43,7 @@ public class InitialSecuritySetup {
} }
userService.migrateOauth2ToSSO(); userService.migrateOauth2ToSSO();
assignUsersToDefaultTeamIfMissing();
initializeInternalApiUser(); initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) { } catch (IllegalArgumentException | SQLException | UnsupportedProviderException e) {
log.error("Failed to initialize security setup.", e); log.error("Failed to initialize security setup.", e);
@ -47,6 +51,19 @@ public class InitialSecuritySetup {
} }
} }
private void assignUsersToDefaultTeamIfMissing() {
Team defaultTeam = teamService.getOrCreateDefaultTeam();
List<User> usersWithoutTeam = userService.getUsersWithoutTeam();
for (User user : usersWithoutTeam) {
user.setTeam(defaultTeam);
}
userService.saveAll(usersWithoutTeam); // batch save
log.info(
"Assigned {} user(s) without a team to the default team.", usersWithoutTeam.size());
}
private void initializeAdminUser() throws SQLException, UnsupportedProviderException { private void initializeAdminUser() throws SQLException, UnsupportedProviderException {
String initialUsername = String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername(); applicationProperties.getSecurity().getInitialLogin().getUsername();
@ -58,7 +75,9 @@ public class InitialSecuritySetup {
&& !initialPassword.isEmpty() && !initialPassword.isEmpty()
&& userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) { && userService.findByUsernameIgnoreCase(initialUsername).isEmpty()) {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); Team team = teamService.getOrCreateDefaultTeam();
userService.saveUser(
initialUsername, initialPassword, team, Role.ADMIN.getRoleId(), false);
log.info("Admin user created: {}", initialUsername); log.info("Admin user created: {}", initialUsername);
} else { } else {
createDefaultAdminUser(); createDefaultAdminUser();
@ -70,7 +89,9 @@ public class InitialSecuritySetup {
String defaultPassword = "stirling"; String defaultPassword = "stirling";
if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) { if (userService.findByUsernameIgnoreCase(defaultUsername).isEmpty()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true); Team team = teamService.getOrCreateDefaultTeam();
userService.saveUser(
defaultUsername, defaultPassword, team, Role.ADMIN.getRoleId(), true);
log.info("Default admin user created: {}", defaultUsername); log.info("Default admin user created: {}", defaultUsername);
} }
} }
@ -78,10 +99,13 @@ public class InitialSecuritySetup {
private void initializeInternalApiUser() private void initializeInternalApiUser()
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) { if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
Team team = teamService.getOrCreateInternalTeam();
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); team,
Role.INTERNAL_API_USER.getRoleId(),
false);
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId()); log.info("Internal API user created: {}", Role.INTERNAL_API_USER.getRoleId());
} }

View File

@ -1,4 +1,4 @@
package stirling.software.proprietary.security.controller.web; package stirling.software.proprietary.security.config;
import static stirling.software.common.util.ProviderUtils.validateProvider; import static stirling.software.common.util.ProviderUtils.validateProvider;
@ -38,11 +38,14 @@ import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.oauth2.GitHubProvider; import stirling.software.common.model.oauth2.GitHubProvider;
import stirling.software.common.model.oauth2.GoogleProvider; import stirling.software.common.model.oauth2.GoogleProvider;
import stirling.software.common.model.oauth2.KeycloakProvider; import stirling.software.common.model.oauth2.KeycloakProvider;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.Authority; import stirling.software.proprietary.security.model.Authority;
import stirling.software.proprietary.security.model.SessionEntity; import stirling.software.proprietary.security.model.SessionEntity;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry; import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@Controller @Controller
@ -57,16 +60,19 @@ public class AccountWebController {
// Assuming you have a repository for user operations // Assuming you have a repository for user operations
private final UserRepository userRepository; private final UserRepository userRepository;
private final boolean runningEE; private final boolean runningEE;
private final TeamRepository teamRepository;
public AccountWebController( public AccountWebController(
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
SessionPersistentRegistry sessionPersistentRegistry, SessionPersistentRegistry sessionPersistentRegistry,
UserRepository userRepository, UserRepository userRepository,
TeamRepository teamRepository,
@Qualifier("runningEE") boolean runningEE) { @Qualifier("runningEE") boolean runningEE) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.sessionPersistentRegistry = sessionPersistentRegistry; this.sessionPersistentRegistry = sessionPersistentRegistry;
this.userRepository = userRepository; this.userRepository = userRepository;
this.runningEE = runningEE; this.runningEE = runningEE;
this.teamRepository = teamRepository;
} }
@GetMapping("/login") @GetMapping("/login")
@ -210,7 +216,7 @@ public class AccountWebController {
@GetMapping("/adminSettings") @GetMapping("/adminSettings")
public String showAddUserForm( public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) { HttpServletRequest request, Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll(); List<User> allUsers = userRepository.findAllWithTeam();
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status // Map to store session information and user activity status
@ -221,14 +227,27 @@ public class AccountWebController {
while (iterator.hasNext()) { while (iterator.hasNext()) {
User user = iterator.next(); User user = iterator.next();
if (user != null) { if (user != null) {
boolean shouldRemove = false;
// Check if user is an INTERNAL_API_USER
for (Authority authority : user.getAuthorities()) { for (Authority authority : user.getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove(); shouldRemove = true;
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId()); roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
// Break out of the inner loop once the user is removed
break; break;
} }
} }
// Also check if user is part of the Internal team
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
shouldRemove = true;
}
// Remove the user if either condition is true
if (shouldRemove) {
iterator.remove();
continue;
}
// Determine the user's session status and last request time // Determine the user's session status and last request time
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false; boolean hasActiveSession = false;
@ -331,6 +350,13 @@ public class AccountWebController {
model.addAttribute("activeUsers", activeUsers); model.addAttribute("activeUsers", activeUsers);
model.addAttribute("disabledUsers", disabledUsers); model.addAttribute("disabledUsers", disabledUsers);
// Get all teams but filter out the Internal team
List<Team> allTeams = teamRepository.findAll()
.stream()
.filter(team -> !team.getName().equals(stirling.software.proprietary.security.service.TeamService.INTERNAL_TEAM_NAME))
.toList();
model.addAttribute("teams", allTeams);
model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers()); model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers());
return "adminSettings"; return "adminSettings";
} }

View File

@ -0,0 +1,11 @@
package stirling.software.proprietary.security.config;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Annotation to mark endpoints that require a Pro or higher license. */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PremiumEndpoint {}

View File

@ -0,0 +1,30 @@
package stirling.software.proprietary.security.config;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
@Aspect
@Component
public class PremiumEndpointAspect {
private final boolean runningProOrHigher;
public PremiumEndpointAspect(@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
this.runningProOrHigher = runningProOrHigher;
}
@Around(
"@annotation(stirling.software.proprietary.security.config.PremiumEndpoint) || @within(stirling.software.proprietary.security.config.PremiumEndpoint)")
public Object checkPremiumAccess(ProceedingJoinPoint joinPoint) throws Throwable {
if (!runningProOrHigher) {
throw new ResponseStatusException(
HttpStatus.FORBIDDEN, "This endpoint requires a Pro or higher license");
}
return joinPoint.proceed();
}
}

View File

@ -9,6 +9,7 @@ import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.jdbc.DatabaseDriver; import org.springframework.boot.jdbc.DatabaseDriver;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import lombok.Getter; import lombok.Getter;
@ -21,8 +22,12 @@ import stirling.software.common.model.exception.UnsupportedProviderException;
@Slf4j @Slf4j
@Getter @Getter
@Configuration @Configuration
@EnableJpaRepositories(basePackages = "stirling.software.proprietary.security.database.repository") @EnableJpaRepositories(
@EntityScan({"stirling.software.proprietary.security.model"}) basePackages = {
"stirling.software.proprietary.security.database.repository",
"stirling.software.proprietary.security.repository"
})
@EntityScan({"stirling.software.proprietary.security.model", "stirling.software.proprietary.model"})
public class DatabaseConfig { public class DatabaseConfig {
public final String DATASOURCE_DEFAULT_URL; public final String DATASOURCE_DEFAULT_URL;
@ -55,6 +60,7 @@ public class DatabaseConfig {
*/ */
@Bean @Bean
@Qualifier("dataSource") @Qualifier("dataSource")
@Primary
public DataSource dataSource() throws UnsupportedProviderException { public DataSource dataSource() throws UnsupportedProviderException {
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create(); DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();

View File

@ -2,9 +2,10 @@ package stirling.software.proprietary.security.configuration.ee;
import static stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License; import static stirling.software.proprietary.security.configuration.ee.KeygenLicenseVerifier.License;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@ -28,18 +29,23 @@ public class EEAppConfig {
migrateEnterpriseSettingsToPremium(this.applicationProperties); migrateEnterpriseSettingsToPremium(this.applicationProperties);
} }
@Profile("security")
@Bean(name = "runningProOrHigher") @Bean(name = "runningProOrHigher")
@Qualifier("runningProOrHigher") @Primary
public boolean runningProOrHigher() { public boolean runningProOrHigher() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL; return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
} }
@Profile("security")
@Bean(name = "license") @Bean(name = "license")
@Primary
public String licenseType() { public String licenseType() {
return licenseKeyChecker.getPremiumLicenseEnabledResult().name(); return licenseKeyChecker.getPremiumLicenseEnabledResult().name();
} }
@Profile("security")
@Bean(name = "runningEE") @Bean(name = "runningEE")
@Primary
public boolean runningEnterprise() { public boolean runningEnterprise() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE; return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE;
} }
@ -49,7 +55,9 @@ public class EEAppConfig {
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin(); return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
} }
@Profile("security")
@Bean(name = "GoogleDriveEnabled") @Bean(name = "GoogleDriveEnabled")
@Primary
public boolean googleDriveEnabled() { public boolean googleDriveEnabled() {
return runningProOrHigher() return runningProOrHigher()
&& applicationProperties.getPremium().getProFeatures().getGoogleDrive().isEnabled(); && applicationProperties.getPremium().getProFeatures().getGoogleDrive().isEnabled();

View File

@ -0,0 +1,127 @@
package stirling.software.proprietary.security.controller.api;
import java.util.Optional;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.view.RedirectView;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.config.PremiumEndpoint;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.TeamService;
@Controller
@RequestMapping("/api/v1/team")
@Tag(name = "Team", description = "Team Management APIs")
@Slf4j
@RequiredArgsConstructor
@PremiumEndpoint
public class TeamController {
private final TeamRepository teamRepository;
private final UserRepository userRepository;
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/create")
public RedirectView createTeam(@RequestParam("name") String name) {
if (teamRepository.existsByNameIgnoreCase(name)) {
return new RedirectView("/adminSettings?messageType=teamExists");
}
Team team = new Team();
team.setName(name);
teamRepository.save(team);
return new RedirectView("/adminSettings?messageType=teamCreated");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/rename")
public RedirectView renameTeam(
@RequestParam("teamId") Long teamId, @RequestParam("newName") String newName) {
Optional<Team> existing = teamRepository.findById(teamId);
if (existing.isEmpty()) {
return new RedirectView("/adminSettings?messageType=teamNotFound");
}
if (teamRepository.existsByNameIgnoreCase(newName)) {
return new RedirectView("/adminSettings?messageType=teamNameExists");
}
Team team = existing.get();
// Prevent renaming the Internal team
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible");
}
team.setName(newName);
teamRepository.save(team);
return new RedirectView("/adminSettings?messageType=teamRenamed");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/delete")
@Transactional
public RedirectView deleteTeam(@RequestParam("teamId") Long teamId) {
Optional<Team> teamOpt = teamRepository.findById(teamId);
if (teamOpt.isEmpty()) {
return new RedirectView("/adminSettings?messageType=teamNotFound");
}
Team team = teamOpt.get();
// Prevent deleting the Internal team
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible");
}
long memberCount = userRepository.countByTeam(team);
if (memberCount > 0) {
return new RedirectView("/adminSettings?messageType=teamHasUsers");
}
teamRepository.delete(team);
return new RedirectView("/adminSettings?messageType=teamDeleted");
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/addUser")
@Transactional
public RedirectView addUserToTeam(
@RequestParam("teamId") Long teamId,
@RequestParam("userId") Long userId) {
// Find the team
Team team = teamRepository.findById(teamId)
.orElseThrow(() -> new RuntimeException("Team not found"));
// Prevent adding users to the Internal team
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return new RedirectView("/teams?error=internalTeamNotAccessible");
}
// Find the user
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found"));
// Check if user is in the Internal team - prevent moving them
if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers");
}
// Assign user to team
user.setTeam(team);
userRepository.save(user);
// Redirect back to team details page
return new RedirectView("/teams/" + teamId + "?messageType=userAdded");
}
}

View File

@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -32,10 +33,14 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role; import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.api.user.UsernameAndPass; import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.service.TeamService;
import stirling.software.proprietary.security.service.UserService; import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry; import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@ -50,7 +55,8 @@ public class UserController {
private final UserService userService; private final UserService userService;
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final TeamRepository teamRepository;
private final UserRepository userRepository;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
@ -60,7 +66,13 @@ public class UserController {
return "register"; return "register";
} }
try { try {
userService.saveUser(requestModel.getUsername(), requestModel.getPassword()); Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
userService.saveUser(
requestModel.getUsername(),
requestModel.getPassword(),
team,
Role.USER.getRoleId(),
false);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return "redirect:/login?messageType=invalidUsername"; return "redirect:/login?messageType=invalidUsername";
} }
@ -200,6 +212,7 @@ public class UserController {
@RequestParam(name = "username", required = true) String username, @RequestParam(name = "username", required = true) String username,
@RequestParam(name = "password", required = false) String password, @RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
@RequestParam(name = "authType") String authType, @RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
@ -233,13 +246,29 @@ public class UserController {
// If the role ID is not valid, redirect with an error message // If the role ID is not valid, redirect with an error message
return new RedirectView("/adminSettings?messageType=invalidRole", true); return new RedirectView("/adminSettings?messageType=invalidRole", true);
} }
// Use teamId if provided, otherwise use default team
Long effectiveTeamId = teamId;
if (effectiveTeamId == null) {
Team defaultTeam = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
if (defaultTeam != null) {
effectiveTeamId = defaultTeam.getId();
}
} else {
// Check if the selected team is Internal - prevent assigning to it
Team selectedTeam = teamRepository.findById(effectiveTeamId).orElse(null);
if (selectedTeam != null && TeamService.INTERNAL_TEAM_NAME.equals(selectedTeam.getName())) {
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true);
}
}
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role); userService.saveUser(username, AuthenticationType.SSO, effectiveTeamId, role);
} else { } else {
if (password.isBlank()) { if (password.isBlank()) {
return new RedirectView("/adminSettings?messageType=invalidPassword", true); return new RedirectView("/adminSettings?messageType=invalidPassword", true);
} }
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, effectiveTeamId, role, forceChange);
} }
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/adminSettings", // Redirect to account page after adding the user
@ -248,9 +277,11 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeRole") @PostMapping("/admin/changeRole")
@Transactional
public RedirectView changeRole( public RedirectView changeRole(
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
Authentication authentication) Authentication authentication)
throws SQLException, UnsupportedProviderException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@ -278,6 +309,26 @@ public class UserController {
return new RedirectView("/adminSettings?messageType=invalidRole", true); return new RedirectView("/adminSettings?messageType=invalidRole", true);
} }
User user = userOpt.get(); User user = userOpt.get();
// Update the team if a teamId is provided
if (teamId != null) {
Team team = teamRepository.findById(teamId).orElse(null);
if (team != null) {
// Prevent assigning to Internal team
if (TeamService.INTERNAL_TEAM_NAME.equals(team.getName())) {
return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true);
}
// Prevent moving users from Internal team
if (user.getTeam() != null && TeamService.INTERNAL_TEAM_NAME.equals(user.getTeam().getName())) {
return new RedirectView("/adminSettings?messageType=cannotMoveInternalUsers", true);
}
user.setTeam(team);
userRepository.save(user);
}
}
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/adminSettings", // Redirect to account page after adding the user

View File

@ -0,0 +1,105 @@
package stirling.software.proprietary.security.controller.web;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
import stirling.software.proprietary.security.database.repository.SessionRepository;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.service.TeamService;
@Controller
@RequestMapping("/teams")
@RequiredArgsConstructor
@Slf4j
public class TeamWebController {
private final TeamRepository teamRepository;
private final SessionRepository sessionRepository;
private final UserRepository userRepository;
@GetMapping
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String listTeams(Model model) {
// Get teams with user counts using a DTO projection
List<TeamWithUserCountDTO> allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount();
// Filter out the Internal team
List<TeamWithUserCountDTO> teamsWithCounts = allTeamsWithCounts.stream()
.filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME))
.toList();
// Get the latest activity for each team
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
// Convert the query results to a map for easy access in the view
Map<Long, Date> teamLastRequest = new HashMap<>();
for (Object[] result : teamActivities) {
Long teamId = (Long) result[0]; // teamId alias
Date lastActivity = (Date) result[1]; // lastActivity alias
teamLastRequest.put(teamId, lastActivity);
}
// Add data to the model
model.addAttribute("teamsWithCounts", teamsWithCounts);
model.addAttribute("teamLastRequest", teamLastRequest);
return "accounts/teams";
}
@GetMapping("/{id}")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String viewTeamDetails(@PathVariable("id") Long id, Model model) {
// Get the team
Team team = teamRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Team not found"));
// Prevent access to Internal team
if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) {
return "redirect:/teams?error=internalTeamNotAccessible";
}
// Get users for this team directly using the direct query
List<User> teamUsers = userRepository.findAllByTeamId(id);
// Get all users not in this team for the Add User to Team dropdown
// Exclude users that are in the Internal team
List<User> allUsers = userRepository.findAllWithTeam();
List<User> availableUsers = allUsers.stream()
.filter(user -> (user.getTeam() == null || !user.getTeam().getId().equals(id)) &&
(user.getTeam() == null || !user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)))
.toList();
// Get the latest session for each user in the team
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
// Create a map of username to last request date
Map<String, Date> userLastRequest = new HashMap<>();
for (Object[] result : userSessions) {
String username = (String) result[0]; // username alias
Date lastRequest = (Date) result[1]; // lastRequest alias
userLastRequest.put(username, lastRequest);
}
model.addAttribute("team", team);
model.addAttribute("teamUsers", teamUsers);
model.addAttribute("availableUsers", availableUsers);
model.addAttribute("userLastRequest", userLastRequest);
return "accounts/team-details";
}
}

View File

@ -29,4 +29,20 @@ public interface SessionRepository extends JpaRepository<SessionEntity, String>
@Param("expired") boolean expired, @Param("expired") boolean expired,
@Param("lastRequest") Date lastRequest, @Param("lastRequest") Date lastRequest,
@Param("principalName") String principalName); @Param("principalName") String principalName);
@Query(
"SELECT t.id as teamId, MAX(s.lastRequest) as lastActivity "
+ "FROM stirling.software.proprietary.model.Team t "
+ "LEFT JOIN t.users u "
+ "LEFT JOIN SessionEntity s ON u.username = s.principalName "
+ "GROUP BY t.id")
List<Object[]> findLatestActivityByTeam();
@Query(
"SELECT u.username as username, MAX(s.lastRequest) as lastRequest "
+ "FROM stirling.software.proprietary.security.model.User u "
+ "LEFT JOIN SessionEntity s ON u.username = s.principalName "
+ "WHERE u.team.id = :teamId "
+ "GROUP BY u.username")
List<Object[]> findLatestSessionByTeamId(@Param("teamId") Long teamId);
} }

View File

@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
@Repository @Repository
@ -22,4 +23,17 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByApiKey(String apiKey); Optional<User> findByApiKey(String apiKey);
List<User> findByAuthenticationTypeIgnoreCase(String authenticationType); List<User> findByAuthenticationTypeIgnoreCase(String authenticationType);
@Query("SELECT u FROM User u WHERE u.team IS NULL")
List<User> findAllWithoutTeam();
@Query(value = "SELECT u FROM User u LEFT JOIN FETCH u.team")
List<User> findAllWithTeam();
@Query("SELECT u FROM User u JOIN FETCH u.authorities JOIN FETCH u.team WHERE u.team.id = :teamId")
List<User> findAllByTeamId(@Param("teamId") Long teamId);
long countByTeam(Team team);
List<User> findAllByTeam(Team team);
} }

View File

@ -16,6 +16,7 @@ import lombok.Setter;
import lombok.ToString; import lombok.ToString;
import stirling.software.common.model.enumeration.Role; import stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.model.Team;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
@ -57,6 +58,10 @@ public class User implements Serializable {
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>(); private Set<Authority> authorities = new HashSet<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@ElementCollection @ElementCollection
@MapKeyColumn(name = "setting_key") @MapKeyColumn(name = "setting_key")
@Lob @Lob

View File

@ -0,0 +1,23 @@
package stirling.software.proprietary.security.repository;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
@Repository
public interface TeamRepository extends JpaRepository<Team, Long> {
Optional<Team> findByName(String name);
@Query("SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) " +
"FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
boolean existsByNameIgnoreCase(String name);
}

View File

@ -0,0 +1,40 @@
package stirling.software.proprietary.security.service;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.repository.TeamRepository;
@Service
@RequiredArgsConstructor
public class TeamService {
private final TeamRepository teamRepository;
public static final String DEFAULT_TEAM_NAME = "Default";
public static final String INTERNAL_TEAM_NAME = "Internal";
public Team getOrCreateDefaultTeam() {
return teamRepository
.findByName(DEFAULT_TEAM_NAME)
.orElseGet(
() -> {
Team defaultTeam = new Team();
defaultTeam.setName(DEFAULT_TEAM_NAME);
return teamRepository.save(defaultTeam);
});
}
public Team getOrCreateInternalTeam() {
return teamRepository
.findByName(INTERNAL_TEAM_NAME)
.orElseGet(
() -> {
Team internalTeam = new Team();
internalTeam.setName(INTERNAL_TEAM_NAME);
return teamRepository.save(internalTeam);
});
}
}

View File

@ -8,6 +8,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
@ -31,11 +32,13 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role; import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.common.service.UserServiceInterface; import stirling.software.common.service.UserServiceInterface;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.AuthorityRepository; import stirling.software.proprietary.security.database.repository.AuthorityRepository;
import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.Authority; import stirling.software.proprietary.security.model.Authority;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.proprietary.security.session.SessionPersistentRegistry; import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@ -45,7 +48,7 @@ import stirling.software.proprietary.security.session.SessionPersistentRegistry;
public class UserService implements UserServiceInterface { public class UserService implements UserServiceInterface {
private final UserRepository userRepository; private final UserRepository userRepository;
private final TeamRepository teamRepository;
private final AuthorityRepository authorityRepository; private final AuthorityRepository authorityRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
@ -162,7 +165,7 @@ public class UserService implements UserServiceInterface {
public void saveUser(String username, AuthenticationType authenticationType) public void saveUser(String username, AuthenticationType authenticationType)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, authenticationType, Role.USER.getRoleId()); saveUser(username, authenticationType, (Long) null, Role.USER.getRoleId());
} }
private User saveUser(Optional<User> user, String apiKey) { private User saveUser(Optional<User> user, String apiKey) {
@ -173,71 +176,98 @@ public class UserService implements UserServiceInterface {
throw new UsernameNotFoundException("User not found"); throw new UsernameNotFoundException("User not found");
} }
public void saveUser(String username, AuthenticationType authenticationType, String role) public User saveUser(
String username, AuthenticationType authenticationType, Long teamId, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { return saveUserCore(
throw new IllegalArgumentException(getInvalidUsernameMessage()); username, // username
} null, // password
User user = new User(); authenticationType, // authenticationType
user.setUsername(username); teamId, // teamId
user.setEnabled(true); null, // team
user.setFirstLogin(false); role, // role
user.addAuthority(new Authority(role, user)); false, // firstLogin
user.setAuthenticationType(authenticationType); true // enabled
userRepository.save(user); );
databaseService.exportDatabase();
} }
public void saveUser(String username, String password) public User saveUser(
String username, AuthenticationType authenticationType, Team team, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { return saveUserCore(
throw new IllegalArgumentException(getInvalidUsernameMessage()); username, // username
} null, // password
User user = new User(); authenticationType, // authenticationType
user.setUsername(username); null, // teamId
user.setPassword(passwordEncoder.encode(password)); team, // team
user.setEnabled(true); role, // role
user.setAuthenticationType(AuthenticationType.WEB); false, // firstLogin
user.addAuthority(new Authority(Role.USER.getRoleId(), user)); true // enabled
userRepository.save(user); );
databaseService.exportDatabase();
} }
public void saveUser(String username, String password, String role, boolean firstLogin) public User saveUser(String username, String password, Long teamId)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { return saveUserCore(
throw new IllegalArgumentException(getInvalidUsernameMessage()); username, // username
} password, // password
User user = new User(); AuthenticationType.WEB, // authenticationType
user.setUsername(username); teamId, // teamId
user.setPassword(passwordEncoder.encode(password)); null, // team
user.addAuthority(new Authority(role, user)); Role.USER.getRoleId(), // role
user.setEnabled(true); false, // firstLogin
user.setAuthenticationType(AuthenticationType.WEB); true // enabled
user.setFirstLogin(firstLogin); );
userRepository.save(user);
databaseService.exportDatabase();
} }
public void saveUser(String username, String password, String role) public User saveUser(
String username, String password, Team team, String role, boolean firstLogin)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, password, role, false); return saveUserCore(
username, // username
password, // password
AuthenticationType.WEB, // authenticationType
null, // teamId
team, // team
role, // role
firstLogin, // firstLogin
true // enabled
);
} }
public void saveUser(String username, String password, boolean firstLogin, boolean enabled) public User saveUser(
String username, String password, Long teamId, String role, boolean firstLogin)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) { return saveUserCore(
throw new IllegalArgumentException(getInvalidUsernameMessage()); username, // username
} password, // password
User user = new User(); AuthenticationType.WEB, // authenticationType
user.setUsername(username); teamId, // teamId
user.setPassword(passwordEncoder.encode(password)); null, // team
user.addAuthority(new Authority(Role.USER.getRoleId(), user)); role, // role
user.setEnabled(enabled); firstLogin, // firstLogin
user.setAuthenticationType(AuthenticationType.WEB); true // enabled
user.setFirstLogin(firstLogin); );
userRepository.save(user); }
databaseService.exportDatabase();
public void saveUser(String username, String password, Long teamId, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUser(username, password, teamId, role, false);
}
public void saveUser(
String username, String password, Long teamId, boolean firstLogin, boolean enabled)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
saveUserCore(
username, // username
password, // password
AuthenticationType.WEB, // authenticationType
teamId, // teamId
null, // team
Role.USER.getRoleId(), // role
firstLogin, // firstLogin
enabled // enabled
);
} }
public void deleteUser(String username) { public void deleteUser(String username) {
@ -345,6 +375,111 @@ public class UserService implements UserServiceInterface {
return passwordEncoder.matches(currentPassword, user.getPassword()); return passwordEncoder.matches(currentPassword, user.getPassword());
} }
/**
* Resolves a team based on the provided information, with consistent error handling.
*
* @param teamId The ID of the team to find, may be null
* @param defaultTeamSupplier A supplier that provides a default team when teamId is null
* @return The resolved Team object
* @throws IllegalArgumentException If the teamId is invalid
*/
private Team resolveTeam(Long teamId, Supplier<Team> defaultTeamSupplier) {
if (teamId == null) {
return defaultTeamSupplier.get();
}
return teamRepository
.findById(teamId)
.orElseThrow(() -> new IllegalArgumentException("Invalid team ID: " + teamId));
}
/**
* Gets the default team, creating it if it doesn't exist.
*
* @return The default team
*/
private Team getDefaultTeam() {
return teamRepository
.findByName("Default")
.orElseGet(
() -> {
Team team = new Team();
team.setName("Default");
return teamRepository.save(team);
});
}
/**
* Core implementation for saving a user with all possible parameters. This method centralizes
* the common logic for all saveUser variants.
*
* @param username Username for the new user
* @param password Password for the user (may be null for SSO/OAuth users)
* @param authenticationType Type of authentication (WEB, SSO, etc.)
* @param teamId ID of the team to assign (may be null to use default)
* @param team Team object to assign (takes precedence over teamId if both provided)
* @param role Role to assign to the user
* @param firstLogin Whether this is the user's first login
* @param enabled Whether the user account is enabled
* @return The saved User object
* @throws IllegalArgumentException If username is invalid or team is invalid
* @throws SQLException If database operation fails
* @throws UnsupportedProviderException If provider is not supported
*/
private User saveUserCore(
String username,
String password,
AuthenticationType authenticationType,
Long teamId,
Team team,
String role,
boolean firstLogin,
boolean enabled)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
// Set password if provided
if (password != null && !password.isEmpty()) {
user.setPassword(passwordEncoder.encode(password));
}
// Set authentication type
user.setAuthenticationType(authenticationType);
// Set enabled status
user.setEnabled(enabled);
// Set first login flag
user.setFirstLogin(firstLogin);
// Set role (authority)
if (role == null) {
role = Role.USER.getRoleId();
}
user.addAuthority(new Authority(role, user));
// Resolve and set team
if (team != null) {
user.setTeam(team);
} else {
user.setTeam(resolveTeam(teamId, this::getDefaultTeam));
}
// Save user
userRepository.save(user);
// Export database
databaseService.exportDatabase();
return user;
}
public boolean isUsernameValid(String username) { public boolean isUsernameValid(String username) {
// Checks whether the simple username is formatted correctly // Checks whether the simple username is formatted correctly
// Regular expression for user name: Min. 3 characters, max. 50 characters // Regular expression for user name: Min. 3 characters, max. 50 characters
@ -464,7 +599,6 @@ public class UserService implements UserServiceInterface {
} }
} }
@Override
public long getTotalUsersCount() { public long getTotalUsersCount() {
// Count all users in the database // Count all users in the database
long userCount = userRepository.count(); long userCount = userRepository.count();
@ -474,4 +608,12 @@ public class UserService implements UserServiceInterface {
} }
return userCount; return userCount;
} }
public List<User> getUsersWithoutTeam() {
return userRepository.findAllWithoutTeam();
}
public void saveAll(List<User> users) {
userRepository.saveAll(users);
}
} }

View File

@ -0,0 +1,387 @@
/* modern-tables.css - Professional styling for data tables and related elements */
/* Main container - Reduced max-width from 1100px to 900px */
.data-container {
max-width: 900px;
margin: 2rem auto;
background-color: var(--md-sys-color-surface-container-lowest);
border-radius: 1rem;
padding: 0.5rem;
box-shadow: 0 2px 12px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.05);
}
/* Panel / Card */
.data-panel {
background-color: var(--md-sys-color-surface);
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.08);
overflow: hidden;
}
/* Header */
.data-header {
display: flex;
align-items: center;
padding: 1.25rem 1.5rem;
background-color: var(--md-sys-color-surface-variant);
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.data-title {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.75rem;
}
.data-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
border-radius: 0.5rem;
transition: all 0.2s ease;
}
/* Content area */
.data-body {
padding: 1.5rem;
background-color: var(--md-sys-color-surface-container-low);
border-radius: 0.5rem;
}
/* Action buttons container */
.data-actions {
display: flex;
justify-content: center;
margin: 1rem 0 1.5rem;
gap: 0.75rem;
}
/* Can add these classes for different alignments */
.data-actions-start {
justify-content: flex-start;
}
.data-actions-end {
justify-content: flex-end;
}
/* Button styling */
.data-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-weight: 500;
transition: all 0.2s ease;
border: none;
cursor: pointer;
text-decoration: none;
}
/* Fixed button colors - normal state has more contrast now */
.data-btn-primary {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
.data-btn-primary:hover {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-primary);
box-shadow: 0 2px 4px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.1);
}
.data-btn-secondary {
background-color: var(--md-sys-color-secondary);
color: var(--md-sys-color-on-secondary);
}
.data-btn-secondary:hover {
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-secondary);
box-shadow: 0 2px 4px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.1);
}
.data-btn-danger {
background-color: var(--md-sys-color-error);
color: var(--md-sys-color-on-error);
}
.data-btn-danger:hover {
background-color: var(--md-sys-color-error-container);
color: var(--md-sys-color-error);
box-shadow: 0 2px 4px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.1);
}
.data-btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
/* Icon button */
.data-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
border-radius: 0.5rem;
border: none;
cursor: pointer;
transition: all 0.2s ease;
background-color: transparent;
}
/* Fixed icon button colors */
.data-icon-btn-primary {
background-color: var(--md-sys-color-primary);
color: var(--md-sys-color-on-primary);
}
.data-icon-btn-primary:hover {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-primary);
box-shadow: 0 2px 4px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.1);
}
.data-icon-btn-danger {
background-color: var(--md-sys-color-error);
color: var(--md-sys-color-on-error);
}
.data-icon-btn-danger:hover {
background-color: var(--md-sys-color-error-container);
color: var(--md-sys-color-error);
box-shadow: 0 2px 4px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.1);
}
/* Table styling */
.data-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
}
.data-table th {
text-align: left;
padding: 1rem;
background-color: var(--md-sys-color-surface-variant);
color: var(--md-sys-color-on-surface-variant);
font-weight: 600;
position: sticky;
top: 0;
}
.data-table th:first-child {
border-top-left-radius: 0.5rem;
}
.data-table th:last-child {
border-top-right-radius: 0.5rem;
}
.data-table td {
padding: 1rem;
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
.data-table tr:last-child td {
border-bottom: none;
}
.data-table tr:hover {
background-color: rgba(var(--md-sys-color-surface-variant-rgb), 0.5);
}
/* Table action cells */
.data-action-cell {
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: flex-start;
}
.data-action-cell-center {
justify-content: center;
}
.data-action-cell-end {
justify-content: flex-end;
}
/* Status indicators */
.data-status {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 500;
}
.data-status-success {
background-color: var(--md-sys-color-tertiary-container);
color: var(--md-sys-color-tertiary);
}
.data-status-danger {
background-color: var(--md-sys-color-error-container);
color: var(--md-sys-color-error);
}
.data-status-warning {
background-color: var(--md-sys-color-secondary-container);
color: var(--md-sys-color-secondary);
}
.data-status-info {
background-color: var(--md-sys-color-primary-container);
color: var(--md-sys-color-primary);
}
/* Stats/Info container */
.data-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.data-stat-card {
background-color: var(--md-sys-color-surface-variant);
border-radius: 0.5rem;
padding: 1.25rem;
flex: 1;
min-width: 180px;
box-shadow: 0 2px 8px rgba(var(--md-sys-color-shadow, 0, 0, 0), 0.05);
}
.data-stat-label {
font-size: 0.875rem;
color: var(--md-sys-color-on-surface-variant);
margin-bottom: 0.5rem;
}
.data-stat-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--md-sys-color-on-surface);
}
/* Section title */
.data-section-title {
font-size: 1.25rem;
font-weight: 600;
margin: 1.5rem 0 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--md-sys-color-outline-variant);
}
/* Empty state styling */
.data-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
color: var(--md-sys-color-on-surface-variant);
}
.data-empty-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.7;
}
.data-empty-text {
font-size: 1.125rem;
margin-bottom: 1.5rem;
}
/* Modal styling */
.data-modal {
border-radius: 0.75rem;
overflow: hidden;
}
.data-modal-header {
background-color: var(--md-sys-color-surface-variant);
padding: 1.25rem;
border-bottom: 1px solid var(--md-sys-color-outline-variant);
display: flex;
align-items: center;
justify-content: space-between;
}
.data-modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.75rem;
}
/* Modal close button styling */
.data-btn-close {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: 50%;
background-color: var(--md-sys-color-surface-variant);
color: var(--md-sys-color-on-surface-variant);
border: none;
cursor: pointer;
transition: all 0.2s ease;
padding: 0;
margin: 0;
}
.data-btn-close:hover {
background-color: var(--md-sys-color-surface-container-high);
color: var(--md-sys-color-on-surface);
}
.data-btn-close .material-symbols-rounded {
font-size: 1.25rem;
}
.data-modal-body {
padding: 1.5rem;
}
.data-modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--md-sys-color-outline-variant);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
/* Form elements */
.data-form-group {
margin-bottom: 1.25rem;
}
.data-form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.data-form-control {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.5rem;
border: 1px solid
}

View File

@ -0,0 +1,196 @@
<!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=#{team.details.title}, header=#{team.details.header})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">group</span>
</span>
<span th:text="'Team: ' + ${team.name}">Team Name</span>
</h1>
</div>
<div class="data-body">
<div class="data-stats">
<div class="data-stat-card">
<div class="data-stat-label">Total Members:</div>
<div class="data-stat-value" th:text="${teamUsers.size()}">1</div>
</div>
</div>
<div class="data-actions data-actions-start">
<a th:href="@{'/teams'}" class="data-btn data-btn-secondary">
<span class="material-symbols-rounded">arrow_back</span>
<span th:text="#{team.back}">Back to Teams</span>
</a>
</div>
<div class="data-section-title">Members</div>
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Role</th>
<th scope="col" th:title="${@runningProOrHigher} ? #{adminUserSettings.lastRequest} : 'Pro feature'" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr th:each="user : ${teamUsers}">
<td th:text="${user.id}">1</td>
<td th:text="${user.username}">username</td>
<td th:text="#{${user.roleName}}">Role</td>
<td th:text="${@runningProOrHigher} ? (${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'">2023-01-01 12:00:00</td>
<td>
<span th:if="${user.enabled}" class="data-status data-status-success">
<span class="material-symbols-rounded">person</span>
Enabled
</span>
<span th:unless="${user.enabled}" class="data-status data-status-danger">
<span class="material-symbols-rounded">person_off</span>
Disabled
</span>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty state for when there are no team members -->
<div th:if="${teamUsers.empty}" class="data-empty">
<span class="material-symbols-rounded data-empty-icon">person_off</span>
<p class="data-empty-text">This team has no members yet.</p>
<button data-bs-toggle="modal" data-bs-target="#addUserToTeamModal" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{team.addUser}">Add User to Team</span>
</button>
</div>
<!-- Add button for non-empty teams too -->
<div th:if="${!teamUsers.empty}" class="data-actions data-mt-3">
<button data-bs-toggle="modal" data-bs-target="#addUserToTeamModal" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">person_add</span>
<span th:text="#{team.addUser}">Add User to Team</span>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- JavaScript for team warning -->
<script th:inline="javascript">
function checkUserTeam(userId) {
// Clear any existing warning
const warningDiv = document.getElementById('teamChangeWarning');
const warningMessage = document.getElementById('warningMessage');
const submitButton = document.getElementById('addUserSubmitBtn');
// Reset
warningDiv.style.display = 'none';
submitButton.onclick = null;
// Get the selected option
const selectedOption = document.querySelector('#userId option[value="' + userId + '"]');
if (!selectedOption) return;
// Get team data
const currentTeam = selectedOption.getAttribute('data-team');
const currentTeamId = selectedOption.getAttribute('data-team-id');
const newTeamName = /*[[${team.name}]]*/ 'Current Team';
// If user is already in a team, show warning
if (currentTeam && currentTeam.length > 0) {
// Use internationalized message
const warningTemplate = /*[[#{team.warning.moveUser}]]*/ 'Warning: This will move the user from "{0}" team to "{1}" team. Are you sure?';
const formattedWarning = warningTemplate.replace('{0}', currentTeam).replace('{1}', newTeamName);
warningMessage.textContent = formattedWarning;
warningDiv.style.display = 'block';
// Add confirmation to submit button
submitButton.onclick = function(e) {
// Use internationalized message
const confirmTemplate = /*[[#{team.confirm.moveUser}]]*/ 'Are you sure you want to move this user from "{0}" team to "{1}" team?';
const formattedConfirm = confirmTemplate.replace('{0}', currentTeam).replace('{1}', newTeamName);
if (!confirm(formattedConfirm)) {
e.preventDefault();
return false;
}
return true;
};
}
}
</script>
<!-- Add User to Team Modal -->
<div class="modal fade" id="addUserToTeamModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/team/addUser'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">person_add</span>
</span>
<span th:text="#{team.addUser}">Add User to Team</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<input type="hidden" name="teamId" th:value="${team.id}" />
<div class="data-form-group">
<label for="userId" class="data-form-label" th:text="#{team.selectUser}">Select User</label>
<select name="userId" id="userId" class="data-form-control" required onchange="checkUserTeam(this.value)">
<option value="" disabled selected th:text="#{selectFillter}">-- Select User --</option>
<option th:each="user : ${availableUsers}"
th:value="${user.id}"
th:text="${user.username}"
th:data-team="${user.team != null ? user.team.name : ''}"
th:data-team-id="${user.team != null ? user.team.id : ''}">
Username
</option>
</select>
</div>
<!-- Warning message for users being moved between teams -->
<div id="teamChangeWarning" class="alert alert-warning mt-3" style="display: none;">
<span class="material-symbols-rounded">warning</span>
<span id="warningMessage">Warning: This will move the user from their current team to this team.</span>
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" id="addUserSubmitBtn" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{team.addUser}">Add User</span>
</button>
</div>
</div>
</form>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,133 @@
<!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=#{adminUserSettings.manageTeams}, header=#{adminUserSettings.manageTeams})}"></th:block>
<link rel="stylesheet" th:href="@{/css/modern-tables.css}">
</head>
<body>
<th:block th:insert="~{fragments/common :: game}"></th:block>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<div class="data-container">
<div class="data-panel">
<div class="data-header">
<h1 class="data-title">
<span class="data-icon">
<span class="material-symbols-rounded">groups</span>
</span>
<span th:text="#{adminUserSettings.manageTeams}">Team Management</span>
</h1>
</div>
<div class="data-body">
<!-- Back Button -->
<div class="data-actions data-actions-start">
<a href="/adminSettings" class="data-btn data-btn-secondary">
<span class="material-symbols-rounded">arrow_back</span>
<span th:text="#{back.toSettings}">Back to Settings</span>
</a>
</div>
<!-- Create New Team Button -->
<div class="data-actions">
<a href="#"
th:data-bs-toggle="${@runningProOrHigher} ? 'modal' : null"
th:data-bs-target="${@runningProOrHigher} ? '#addTeamModal' : null"
th:class="${@runningProOrHigher} ? 'data-btn data-btn-primary' : 'data-btn data-btn-danger'"
th:title="${@runningProOrHigher} ? #{adminUserSettings.createTeam} : #{enterpriseEdition.proTeamFeatureDisabled}">
<span class="material-symbols-rounded">group_add</span>
<span th:text="#{adminUserSettings.createTeam}">Create New Team</span>
</a>
</div>
<!-- Team Table -->
<div class="table-responsive">
<table class="data-table">
<thead>
<tr>
<th scope="col" th:text="#{adminUserSettings.teamName}">Team Name</th>
<th scope="col" th:text="#{adminUserSettings.totalMembers}">Total Members</th>
<th scope="col" th:title="${@runningProOrHigher} ? #{adminUserSettings.lastRequest} : 'Pro feature'" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:text="#{adminUserSettings.actions}">Actions</th>
</tr>
</thead>
<tbody>
<!-- Try approach 1 - DTO projection -->
<tr th:each="teamDto : ${teamsWithCounts}">
<td th:text="${teamDto.name}"></td>
<td th:text="${teamDto.userCount}"></td>
<td th:text="${@runningProOrHigher} ? (${teamLastRequest[teamDto.id] != null ? #dates.format(teamLastRequest[teamDto.id], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'"></td>
<td>
<div class="data-action-cell">
<a th:href="@{'/teams/' + ${teamDto.id}}" class="data-btn data-btn-secondary data-btn-sm" th:title="#{adminUserSettings.viewTeam}">
<span class="material-symbols-rounded">search</span> <span th:text="#{view}">View</span>
</a>
<form th:action="@{'/api/v1/team/delete'}" method="post" style="display:inline-block"
onsubmit="return confirmDeleteTeam()">
<input type="hidden" name="teamId" th:value="${teamDto.id}" />
<button type="submit" class="data-btn data-btn-danger data-btn-sm"
th:disabled="${!@runningProOrHigher}"
th:classappend="${!@runningProOrHigher} ? 'disabled' : ''"
th:title="${@runningProOrHigher} ? #{adminUserSettings.deleteTeam} : #{enterpriseEdition.proTeamFeatureDisabled}">
<span class="material-symbols-rounded">delete</span> <span th:text="#{delete}">Delete</span>
</button>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Delete Confirmation Script -->
<script th:inline="javascript">
const confirmDeleteText = /*[[#{adminUserSettings.confirmDeleteTeam}]]*/ 'Are you sure you want to delete this team?';
function confirmDeleteTeam() {
return confirm(confirmDeleteText);
}
</script>
</div>
</div>
</div>
<!-- Add Team Modal -->
<div class="modal fade" id="addTeamModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<form th:action="@{'/api/v1/team/create'}" method="post" class="modal-content data-modal">
<div class="data-modal-header">
<h5 class="data-modal-title">
<span class="data-icon">
<span class="material-symbols-rounded">group_add</span>
</span>
<span th:text="#{adminUserSettings.createTeam}">Create Team</span>
</h5>
<button type="button" class="data-btn-close" data-bs-dismiss="modal" aria-label="Close">
<span class="material-symbols-rounded">close</span>
</button>
</div>
<div class="data-modal-body">
<div class="data-form-group">
<label for="teamName" class="data-form-label" th:text="#{adminUserSettings.teamName}">Team Name</label>
<input type="text" name="name" id="teamName" class="data-form-control" required />
</div>
<div class="data-form-actions">
<button type="button" class="data-btn data-btn-secondary" data-bs-dismiss="modal">
<span class="material-symbols-rounded">close</span>
<span th:text="#{cancel}">Cancel</span>
</button>
<button type="submit" class="data-btn data-btn-primary">
<span class="material-symbols-rounded">check</span>
<span th:text="#{adminUserSettings.createTeam}">Create</span>
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>

View File

@ -0,0 +1,83 @@
package stirling.software.proprietary.security.service;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.repository.TeamRepository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class TeamServiceTest {
@Mock
private TeamRepository teamRepository;
@InjectMocks
private TeamService teamService;
@Test
void getDefaultTeam() {
var team = new Team();
team.setName("Marleyans");
when(teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME))
.thenReturn(Optional.of(team));
Team result = teamService.getOrCreateDefaultTeam();
assertEquals(team, result);
}
@Test
void createDefaultTeam_whenRepositoryIsEmpty() {
String teamName = "Default";
var defaultTeam = new Team();
defaultTeam.setId(1L);
defaultTeam.setName(teamName);
when(teamRepository.findByName(teamName))
.thenReturn(Optional.empty());
when(teamRepository.save(any(Team.class))).thenReturn(defaultTeam);
Team result = teamService.getOrCreateDefaultTeam();
assertEquals(TeamService.DEFAULT_TEAM_NAME, result.getName());
}
@Test
void getInternalTeam() {
var team = new Team();
team.setName("Eldians");
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
.thenReturn(Optional.of(team));
Team result = teamService.getOrCreateInternalTeam();
assertEquals(team, result);
}
@Test
void createInternalTeam_whenRepositoryIsEmpty() {
String teamName = "Internal";
Team internalTeam = new Team();
internalTeam.setId(2L);
internalTeam.setName(teamName);
when(teamRepository.findByName(teamName))
.thenReturn(Optional.empty());
when(teamRepository.save(any(Team.class))).thenReturn(internalTeam);
when(teamRepository.findByName(TeamService.INTERNAL_TEAM_NAME))
.thenReturn(Optional.empty());
Team result = teamService.getOrCreateInternalTeam();
assertEquals(internalTeam, result);
}
}

View File

@ -0,0 +1,317 @@
package stirling.software.proprietary.security.service;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.AuthorityRepository;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.repository.TeamRepository;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private TeamRepository teamRepository;
@Mock
private AuthorityRepository authorityRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private MessageSource messageSource;
@Mock
private SessionPersistentRegistry sessionPersistentRegistry;
@Mock
private DatabaseServiceInterface databaseService;
@Mock
private ApplicationProperties.Security.OAUTH2 oauth2Properties;
@InjectMocks
private UserService userService;
private Team mockTeam;
private User mockUser;
@BeforeEach
void setUp() {
mockTeam = new Team();
mockTeam.setId(1L);
mockTeam.setName("Test Team");
mockUser = new User();
mockUser.setId(1L);
mockUser.setUsername("testuser");
mockUser.setEnabled(true);
}
@Test
void testSaveUser_WithUsernameAndAuthenticationType_Success() throws Exception {
// Given
String username = "testuser";
AuthenticationType authType = AuthenticationType.WEB;
when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
userService.saveUser(username, authType);
// Then
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithUsernamePasswordAndTeamId_Success() throws Exception {
// Given
String username = "testuser";
String password = "password123";
Long teamId = 1L;
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
User result = userService.saveUser(username, password, teamId);
// Then
assertNotNull(result);
verify(passwordEncoder).encode(password);
verify(teamRepository).findById(teamId);
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithTeamAndRole_Success() throws Exception {
// Given
String username = "testuser";
String password = "password123";
String role = Role.ADMIN.getRoleId();
boolean firstLogin = true;
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
User result = userService.saveUser(username, password, mockTeam, role, firstLogin);
// Then
assertNotNull(result);
verify(passwordEncoder).encode(password);
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithInvalidUsername_ThrowsException() throws Exception {
// Given
String invalidUsername = "ab"; // Too short (less than 3 characters)
AuthenticationType authType = AuthenticationType.WEB;
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> userService.saveUser(invalidUsername, authType)
);
verify(userRepository, never()).save(any(User.class));
verify(databaseService, never()).exportDatabase();
}
@Test
void testSaveUser_WithNullPassword_Success() throws Exception {
// Given
String username = "testuser";
Long teamId = 1L;
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
User result = userService.saveUser(username, null, teamId);
// Then
assertNotNull(result);
verify(passwordEncoder, never()).encode(anyString());
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithEmptyPassword_Success() throws Exception {
// Given
String username = "testuser";
String emptyPassword = "";
Long teamId = 1L;
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
User result = userService.saveUser(username, emptyPassword, teamId);
// Then
assertNotNull(result);
verify(passwordEncoder, never()).encode(anyString());
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithValidEmail_Success() throws Exception {
// Given
String emailUsername = "test@example.com";
AuthenticationType authType = AuthenticationType.SSO;
when(teamRepository.findByName("Default")).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
userService.saveUser(emailUsername, authType);
// Then
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithReservedUsername_ThrowsException() throws Exception {
// Given
String reservedUsername = "all_users";
AuthenticationType authType = AuthenticationType.WEB;
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> userService.saveUser(reservedUsername, authType)
);
verify(userRepository, never()).save(any(User.class));
verify(databaseService, never()).exportDatabase();
}
@Test
void testSaveUser_WithAnonymousUser_ThrowsException() throws Exception {
// Given
String anonymousUsername = "anonymoususer";
AuthenticationType authType = AuthenticationType.WEB;
// When & Then
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> userService.saveUser(anonymousUsername, authType)
);
verify(userRepository, never()).save(any(User.class));
verify(databaseService, never()).exportDatabase();
}
@Test
void testSaveUser_DatabaseExportThrowsException_StillSavesUser() throws Exception {
// Given
String username = "testuser";
String password = "password123";
Long teamId = 1L;
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doThrow(new SQLException("Database export failed")).when(databaseService).exportDatabase();
// When & Then
assertThrows(SQLException.class, () -> userService.saveUser(username, password, teamId));
// Verify user was still saved before the exception
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithFirstLoginFlag_Success() throws Exception {
// Given
String username = "testuser";
String password = "password123";
Long teamId = 1L;
boolean firstLogin = true;
boolean enabled = false;
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
userService.saveUser(username, password, teamId, firstLogin, enabled);
// Then
verify(passwordEncoder).encode(password);
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
@Test
void testSaveUser_WithCustomRole_Success() throws Exception {
// Given
String username = "testuser";
String password = "password123";
Long teamId = 1L;
String customRole = Role.LIMITED_API_USER.getRoleId();
String encodedPassword = "encodedPassword123";
when(passwordEncoder.encode(password)).thenReturn(encodedPassword);
when(teamRepository.findById(teamId)).thenReturn(Optional.of(mockTeam));
when(userRepository.save(any(User.class))).thenReturn(mockUser);
doNothing().when(databaseService).exportDatabase();
// When
userService.saveUser(username, password, teamId, customRole);
// Then
verify(passwordEncoder).encode(password);
verify(userRepository).save(any(User.class));
verify(databaseService).exportDatabase();
}
}

View File

@ -2,6 +2,6 @@ plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs // Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
} }
rootProject.name = 'Stirling-PDF' rootProject.name = 'Stirling PDF'
include 'stirling-pdf', 'common', 'proprietary' include 'stirling-pdf', 'common', 'proprietary'

View File

@ -193,4 +193,4 @@ id_ed25519.pub
**/jcef-bundle/ **/jcef-bundle/
# node_modules # node_modules
node_modules/ node_modules/

View File

@ -1,8 +1,17 @@
apply plugin: 'org.springframework.boot'
repositories { repositories {
maven { url = 'https://build.shibboleth.net/maven/releases' } maven { url = 'https://build.shibboleth.net/maven/releases' }
maven { url = 'https://maven.pkg.github.com/jcefmaven/jcefmaven' } maven { url = 'https://maven.pkg.github.com/jcefmaven/jcefmaven' }
} }
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
dependencies { dependencies {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false' if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI') || (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
@ -71,25 +80,58 @@ sourceSets {
resources { resources {
srcDirs += ['../configs'] srcDirs += ['../configs']
} }
java {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
exclude 'stirling/software/SPDF/UI/impl/**'
}
}
} }
test {
java {
if (System.getenv('DOCKER_ENABLE_SECURITY') == 'false' || System.getenv('DISABLE_ADDITIONAL_FEATURES') == 'true'
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') == 'true')) {
exclude 'stirling/software/proprietary/security/**'
}
if (System.getenv('STIRLING_PDF_DESKTOP_UI') == 'false') {
exclude 'stirling/software/SPDF/UI/impl/**'
}
}
}
} }
// Disable regular jar
jar { jar {
enabled = false
}
// Configure and enable bootJar for this project
bootJar {
enabled = true
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
zip64 = true zip64 = true
from { // Don't include all dependencies directly like the old jar task did
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } // from {
} // configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
// }
// Exclude signature files to prevent "Invalid signature file digest" errors
exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
exclude 'META-INF/*.EC'
manifest { manifest {
attributes( attributes(
'Main-Class': 'stirling.software.SPDF.SPDFApplication',
'Implementation-Title': 'Stirling-PDF', 'Implementation-Title': 'Stirling-PDF',
'Implementation-Version': project.version 'Implementation-Version': project.version
) )
} }
} }
jar.dependsOn ':common:jar' bootJar.dependsOn ':common:jar'
jar.dependsOn ':proprietary:jar' bootJar.dependsOn ':proprietary:jar'

View File

@ -13,8 +13,6 @@ import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
@ -39,10 +37,6 @@ import stirling.software.common.util.UrlUtils;
"stirling.software.SPDF", "stirling.software.SPDF",
"stirling.software.common", "stirling.software.common",
"stirling.software.proprietary" "stirling.software.proprietary"
},
exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
}) })
public class SPDFApplication { public class SPDFApplication {
@ -208,17 +202,37 @@ public class SPDFApplication {
} }
private static String[] getActiveProfile(String[] args) { private static String[] getActiveProfile(String[] args) {
if (args == null) { // 1. Check for explicitly passed profiles
return new String[] {"default"}; if (args != null) {
} for (String arg : args) {
if (arg.startsWith("--spring.profiles.active=")) {
for (String arg : args) { String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
if (arg.contains("spring.profiles.active")) { if (provided.length > 0) {
return arg.substring(args[0].indexOf('=') + 1).split(", "); log.info("#######0000000000000###############################");
return provided;
}
}
} }
} }
log.info("######################################");
// 2. Detect if SecurityConfiguration is present on classpath
if (isClassPresent(
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
log.info("security");
return new String[] {"security"};
} else {
log.info("default");
return new String[] {"default"};
}
}
return new String[] {"default"}; private static boolean isClassPresent(String className) {
try {
Class.forName(className, false, SPDFApplication.class.getClassLoader());
return true;
} catch (ClassNotFoundException e) {
return false;
}
} }
public static String getStaticBaseUrl() { public static String getStaticBaseUrl() {

View File

@ -14,21 +14,21 @@ import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = private static final List<String> ALLOWED_PARAMS =
Arrays.asList( Arrays.asList(
"lang", "lang",
"endpoint", "endpoint",
"endpoints", "endpoints",
"logout", "logout",
"error", "error",
"errorOAuth", "errorOAuth",
"file", "file",
"messageType", "messageType",
"infoMessage"); "infoMessage");
@Override @Override
public boolean preHandle( public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String queryString = request.getQueryString(); String queryString = request.getQueryString();
if (queryString != null && !queryString.isEmpty()) { if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
@ -69,15 +69,15 @@ public class CleanUrlInterceptor implements HandlerInterceptor {
@Override @Override
public void postHandle( public void postHandle(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
Object handler, Object handler,
ModelAndView modelAndView) {} ModelAndView modelAndView) {}
@Override @Override
public void afterCompletion( public void afterCompletion(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
Object handler, Object handler,
Exception ex) {} Exception ex) {}
} }

View File

@ -39,14 +39,14 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
private void discoverEndpoints() { private void discoverEndpoints() {
try { try {
Map<String, RequestMappingHandlerMapping> mappings = Map<String, RequestMappingHandlerMapping> mappings =
applicationContext.getBeansOfType(RequestMappingHandlerMapping.class); applicationContext.getBeansOfType(RequestMappingHandlerMapping.class);
for (Map.Entry<String, RequestMappingHandlerMapping> entry : mappings.entrySet()) { for (Map.Entry<String, RequestMappingHandlerMapping> entry : mappings.entrySet()) {
RequestMappingHandlerMapping mapping = entry.getValue(); RequestMappingHandlerMapping mapping = entry.getValue();
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods(); Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerEntry : for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerEntry :
handlerMethods.entrySet()) { handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = handlerEntry.getKey(); RequestMappingInfo mappingInfo = handlerEntry.getKey();
HandlerMethod handlerMethod = handlerEntry.getValue(); HandlerMethod handlerMethod = handlerEntry.getValue();
@ -105,7 +105,7 @@ public class EndpointInspector implements ApplicationListener<ContextRefreshedEv
String infoString = mappingInfo.toString(); String infoString = mappingInfo.toString();
if (infoString.contains("{")) { if (infoString.contains("{")) {
String patternsSection = String patternsSection =
infoString.substring(infoString.indexOf("{") + 1, infoString.indexOf("}")); infoString.substring(infoString.indexOf("{") + 1, infoString.indexOf("}"));
for (String pattern : patternsSection.split(",")) { for (String pattern : patternsSection.split(",")) {
pattern = pattern.trim(); pattern = pattern.trim();

View File

@ -18,8 +18,8 @@ public class EndpointInterceptor implements HandlerInterceptor {
@Override @Override
public boolean preHandle( public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
boolean isEnabled; boolean isEnabled;

View File

@ -25,23 +25,23 @@ public class ExternalAppDepConfig {
private final Map<String, List<String>> commandToGroupMapping; private final Map<String, List<String>> commandToGroupMapping;
public ExternalAppDepConfig( public ExternalAppDepConfig(
EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) { EndpointConfiguration endpointConfiguration, RuntimePathConfig runtimePathConfig) {
this.endpointConfiguration = endpointConfiguration; this.endpointConfiguration = endpointConfiguration;
weasyprintPath = runtimePathConfig.getWeasyPrintPath(); weasyprintPath = runtimePathConfig.getWeasyPrintPath();
unoconvPath = runtimePathConfig.getUnoConvertPath(); unoconvPath = runtimePathConfig.getUnoConvertPath();
commandToGroupMapping = commandToGroupMapping =
new HashMap<>() { new HashMap<>() {
{ {
put("soffice", List.of("LibreOffice")); put("soffice", List.of("LibreOffice"));
put(weasyprintPath, List.of("Weasyprint")); put(weasyprintPath, List.of("Weasyprint"));
put("pdftohtml", List.of("Pdftohtml")); put("pdftohtml", List.of("Pdftohtml"));
put(unoconvPath, List.of("Unoconvert")); put(unoconvPath, List.of("Unoconvert"));
put("qpdf", List.of("qpdf")); put("qpdf", List.of("qpdf"));
put("tesseract", List.of("tesseract")); put("tesseract", List.of("tesseract"));
} }
}; };
} }
private boolean isCommandAvailable(String command) { private boolean isCommandAvailable(String command) {
@ -63,8 +63,8 @@ public class ExternalAppDepConfig {
private List<String> getAffectedFeatures(String group) { private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream() return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint)) .map(endpoint -> formatEndpointAsFeature(endpoint))
.toList(); .toList();
} }
private String formatEndpointAsFeature(String endpoint) { private String formatEndpointAsFeature(String endpoint) {
@ -72,8 +72,8 @@ public class ExternalAppDepConfig {
String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image"); String feature = endpoint.replace("-", " ").replace("pdf", "PDF").replace("img", "image");
// Split into words and capitalize each word // Split into words and capitalize each word
return Arrays.stream(feature.split("\\s+")) return Arrays.stream(feature.split("\\s+"))
.map(word -> capitalizeWord(word)) .map(word -> capitalizeWord(word))
.collect(Collectors.joining(" ")); .collect(Collectors.joining(" "));
} }
private String capitalizeWord(String word) { private String capitalizeWord(String word) {
@ -95,12 +95,12 @@ public class ExternalAppDepConfig {
List<String> affectedFeatures = getAffectedFeatures(group); List<String> affectedFeatures = getAffectedFeatures(group);
endpointConfiguration.disableGroup(group); endpointConfiguration.disableGroup(group);
log.warn( log.warn(
"Missing dependency: {} - Disabling group: {} (Affected features: {})", "Missing dependency: {} - Disabling group: {} (Affected features: {})",
command, command,
group, group,
affectedFeatures != null && !affectedFeatures.isEmpty() affectedFeatures != null && !affectedFeatures.isEmpty()
? String.join(", ", affectedFeatures) ? String.join(", ", affectedFeatures)
: "unknown"); : "unknown");
} }
} }
} }
@ -123,9 +123,9 @@ public class ExternalAppDepConfig {
endpointConfiguration.disableGroup("Python"); endpointConfiguration.disableGroup("Python");
endpointConfiguration.disableGroup("OpenCV"); endpointConfiguration.disableGroup("OpenCV");
log.warn( log.warn(
"Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}", "Missing dependency: Python - Disabling Python features: {} and OpenCV features: {}",
String.join(", ", pythonFeatures), String.join(", ", pythonFeatures),
String.join(", ", openCVFeatures)); String.join(", ", openCVFeatures));
} else { } else {
// If Python is available, check for OpenCV // If Python is available, check for OpenCV
try { try {
@ -141,16 +141,16 @@ public class ExternalAppDepConfig {
List<String> openCVFeatures = getAffectedFeatures("OpenCV"); List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("OpenCV"); endpointConfiguration.disableGroup("OpenCV");
log.warn( log.warn(
"OpenCV not available in Python - Disabling OpenCV features: {}", "OpenCV not available in Python - Disabling OpenCV features: {}",
String.join(", ", openCVFeatures)); String.join(", ", openCVFeatures));
} }
} catch (Exception e) { } catch (Exception e) {
List<String> openCVFeatures = getAffectedFeatures("OpenCV"); List<String> openCVFeatures = getAffectedFeatures("OpenCV");
endpointConfiguration.disableGroup("OpenCV"); endpointConfiguration.disableGroup("OpenCV");
log.warn( log.warn(
"Error checking OpenCV: {} - Disabling OpenCV features: {}", "Error checking OpenCV: {} - Disabling OpenCV features: {}",
e.getMessage(), e.getMessage(),
String.join(", ", openCVFeatures)); String.join(", ", openCVFeatures));
} }
} }
endpointConfiguration.logDisabledEndpointsSummary(); endpointConfiguration.logDisabledEndpointsSummary();

View File

@ -25,9 +25,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations( .addResourceLocations(
"file:" + InstallationPathConfig.getStaticPath(), "file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
"classpath:/static/"
);
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/"); registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/"); registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
// .setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching

View File

@ -0,0 +1,266 @@
package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineNode;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/general")
@Slf4j
@Tag(name = "General", description = "General APIs")
@RequiredArgsConstructor
public class EditTableOfContentsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final ObjectMapper objectMapper;
@PostMapping(value = "/extract-bookmarks", consumes = "multipart/form-data")
@Operation(
summary = "Extract PDF Bookmarks",
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
@ResponseBody
public List<Map<String, Object>> extractBookmarks(@RequestParam("file") MultipartFile file)
throws Exception {
PDDocument document = null;
try {
document = pdfDocumentFactory.load(file);
PDDocumentOutline outline = document.getDocumentCatalog().getDocumentOutline();
if (outline == null) {
log.info("No outline/bookmarks found in PDF");
return new ArrayList<>();
}
return extractBookmarkItems(document, outline);
} finally {
if (document != null) {
document.close();
}
}
}
private List<Map<String, Object>> extractBookmarkItems(
PDDocument document, PDDocumentOutline outline) throws Exception {
List<Map<String, Object>> bookmarks = new ArrayList<>();
PDOutlineItem current = outline.getFirstChild();
while (current != null) {
Map<String, Object> bookmark = new HashMap<>();
// Get bookmark title
String title = current.getTitle();
bookmark.put("title", title);
// Get page number (1-based for UI purposes)
PDPage page = current.findDestinationPage(document);
if (page != null) {
int pageIndex = document.getPages().indexOf(page);
bookmark.put("pageNumber", pageIndex + 1);
} else {
bookmark.put("pageNumber", 1);
}
// Process children if any
PDOutlineItem child = current.getFirstChild();
if (child != null) {
List<Map<String, Object>> children = new ArrayList<>();
PDOutlineNode parent = current;
while (child != null) {
// Recursively process child items
Map<String, Object> childBookmark = processChild(document, child);
children.add(childBookmark);
child = child.getNextSibling();
}
bookmark.put("children", children);
} else {
bookmark.put("children", new ArrayList<>());
}
bookmarks.add(bookmark);
current = current.getNextSibling();
}
return bookmarks;
}
private Map<String, Object> processChild(PDDocument document, PDOutlineItem item)
throws Exception {
Map<String, Object> bookmark = new HashMap<>();
// Get bookmark title
String title = item.getTitle();
bookmark.put("title", title);
// Get page number (1-based for UI purposes)
PDPage page = item.findDestinationPage(document);
if (page != null) {
int pageIndex = document.getPages().indexOf(page);
bookmark.put("pageNumber", pageIndex + 1);
} else {
bookmark.put("pageNumber", 1);
}
// Process children if any
PDOutlineItem child = item.getFirstChild();
if (child != null) {
List<Map<String, Object>> children = new ArrayList<>();
while (child != null) {
// Recursively process child items
Map<String, Object> childBookmark = processChild(document, child);
children.add(childBookmark);
child = child.getNextSibling();
}
bookmark.put("children", children);
} else {
bookmark.put("children", new ArrayList<>());
}
return bookmark;
}
@PostMapping(value = "/edit-table-of-contents", consumes = "multipart/form-data")
@Operation(
summary = "Edit Table of Contents",
description = "Add or edit bookmarks/table of contents in a PDF document.")
public ResponseEntity<byte[]> editTableOfContents(
@ModelAttribute EditTableOfContentsRequest request) throws Exception {
MultipartFile file = request.getFileInput();
PDDocument document = null;
try {
document = pdfDocumentFactory.load(file);
// Parse the bookmark data from JSON
List<BookmarkItem> bookmarks =
objectMapper.readValue(
request.getBookmarkData(), new TypeReference<List<BookmarkItem>>() {});
// Create a new document outline
PDDocumentOutline outline = new PDDocumentOutline();
document.getDocumentCatalog().setDocumentOutline(outline);
// Add bookmarks to the outline
addBookmarksToOutline(document, outline, bookmarks);
// Save the document to a byte array
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos);
String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", "");
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), filename + "_with_toc.pdf", MediaType.APPLICATION_PDF);
} finally {
if (document != null) {
document.close();
}
}
}
private void addBookmarksToOutline(
PDDocument document, PDDocumentOutline outline, List<BookmarkItem> bookmarks) {
for (BookmarkItem bookmark : bookmarks) {
PDOutlineItem item = createOutlineItem(document, bookmark);
outline.addLast(item);
if (bookmark.getChildren() != null && !bookmark.getChildren().isEmpty()) {
addChildBookmarks(document, item, bookmark.getChildren());
}
}
}
private void addChildBookmarks(
PDDocument document, PDOutlineItem parent, List<BookmarkItem> children) {
for (BookmarkItem child : children) {
PDOutlineItem item = createOutlineItem(document, child);
parent.addLast(item);
if (child.getChildren() != null && !child.getChildren().isEmpty()) {
addChildBookmarks(document, item, child.getChildren());
}
}
}
private PDOutlineItem createOutlineItem(PDDocument document, BookmarkItem bookmark) {
PDOutlineItem item = new PDOutlineItem();
item.setTitle(bookmark.getTitle());
// Get the target page - adjust for 0-indexed pages in PDFBox
int pageIndex = bookmark.getPageNumber() - 1;
if (pageIndex < 0) {
pageIndex = 0;
} else if (pageIndex >= document.getNumberOfPages()) {
pageIndex = document.getNumberOfPages() - 1;
}
PDPage page = document.getPage(pageIndex);
item.setDestination(page);
return item;
}
// Inner class to represent bookmarks in JSON
public static class BookmarkItem {
private String title;
private int pageNumber;
private List<BookmarkItem> children = new ArrayList<>();
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPageNumber() {
return pageNumber;
}
public void setPageNumber(int pageNumber) {
this.pageNumber = pageNumber;
}
public List<BookmarkItem> getChildren() {
return children;
}
public void setChildren(List<BookmarkItem> children) {
this.children = children;
}
}
}

View File

@ -15,6 +15,8 @@ import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm; import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.apache.pdfbox.pdmodel.interactive.form.PDField; import org.apache.pdfbox.pdmodel.interactive.form.PDField;
import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField; import org.apache.pdfbox.pdmodel.interactive.form.PDSignatureField;
@ -110,6 +112,46 @@ public class MergeController {
} }
} }
// Adds a table of contents to the merged document using filenames as chapter titles
private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) {
// Create the document outline
PDDocumentOutline outline = new PDDocumentOutline();
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
int pageIndex = 0; // Current page index in the merged document
// Iterate through the original files
for (MultipartFile file : files) {
// Get the filename without extension to use as bookmark title
String filename = file.getOriginalFilename();
String title = filename;
if (title != null && title.contains(".")) {
title = title.substring(0, title.lastIndexOf('.'));
}
// Create an outline item for this file
PDOutlineItem item = new PDOutlineItem();
item.setTitle(title);
// Set the destination to the first page of this file in the merged document
if (pageIndex < mergedDocument.getNumberOfPages()) {
PDPage page = mergedDocument.getPage(pageIndex);
item.setDestination(page);
}
// Add the item to the outline
outline.addLast(item);
// Increment page index for the next file
try (PDDocument doc = pdfDocumentFactory.load(file)) {
pageIndex += doc.getNumberOfPages();
} catch (IOException e) {
log.error("Error loading document for TOC generation", e);
pageIndex++; // Increment by at least one if we can't determine page count
}
}
}
@PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs")
@Operation( @Operation(
summary = "Merge multiple PDF files into one", summary = "Merge multiple PDF files into one",
@ -124,6 +166,7 @@ public class MergeController {
PDDocument mergedDocument = null; PDDocument mergedDocument = null;
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign()); boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
boolean generateToc = request.isGenerateToc();
try { try {
MultipartFile[] files = request.getFileInput(); MultipartFile[] files = request.getFileInput();
@ -170,6 +213,11 @@ public class MergeController {
} }
} }
// Add table of contents if generateToc is true
if (generateToc && files.length > 0) {
addTableOfContents(mergedDocument, files);
}
// Save the modified document to a new ByteArrayOutputStream // Save the modified document to a new ByteArrayOutputStream
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
mergedDocument.save(baos); mergedDocument.save(baos);

View File

@ -36,9 +36,9 @@ public class SettingsController {
public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException { public ResponseEntity<String> updateApiKey(@RequestBody Boolean enabled) throws IOException {
if (applicationProperties.getSystem().getEnableAnalytics() != null) { if (applicationProperties.getSystem().getEnableAnalytics() != null) {
return ResponseEntity.status(HttpStatus.ALREADY_REPORTED) return ResponseEntity.status(HttpStatus.ALREADY_REPORTED)
.body( .body(
"Setting has already been set, To adjust please edit " "Setting has already been set, To adjust please edit "
+ InstallationPathConfig.getSettingsPath()); + InstallationPathConfig.getSettingsPath());
} }
GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled); GeneralUtils.saveKeyToSettings("system.enableAnalytics", enabled);
applicationProperties.getSystem().setEnableAnalytics(enabled); applicationProperties.getSystem().setEnableAnalytics(enabled);

View File

@ -27,9 +27,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
import stirling.software.SPDF.model.api.PDFWithPageNums;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")

View File

@ -31,11 +31,11 @@ import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.common.model.PdfMetadata; import stirling.software.common.model.PdfMetadata;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.service.PdfMetadataService; import stirling.software.common.service.PdfMetadataService;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")

View File

@ -31,9 +31,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")

View File

@ -16,8 +16,10 @@ import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames; import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.converters.EmlToPdfRequest; import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
@ -39,9 +41,9 @@ public class ConvertEmlToPDF {
summary = "Convert EML to PDF", summary = "Convert EML to PDF",
description = description =
"This endpoint converts EML (email) files to PDF format with extensive" "This endpoint converts EML (email) files to PDF format with extensive"
+ " customization options. Features include font settings, image constraints, display modes, attachment handling," + " customization options. Features include font settings, image constraints, display modes, attachment handling,"
+ " and HTML debug output. Input: EML file, Output: PDF" + " and HTML debug output. Input: EML file, Output: PDF"
+ " or HTML file. Type: SISO") + " or HTML file. Type: SISO")
public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) { public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
@ -94,7 +96,8 @@ public class ConvertEmlToPDF {
try { try {
byte[] pdfBytes = byte[] pdfBytes =
EmlToPdf.convertEmlToPdf( EmlToPdf.convertEmlToPdf(
runtimePathConfig.getWeasyPrintPath(), // Use configured WeasyPrint path runtimePathConfig
.getWeasyPrintPath(), // Use configured WeasyPrint path
request, request,
fileBytes, fileBytes,
originalFilename, originalFilename,
@ -119,12 +122,20 @@ public class ConvertEmlToPDF {
.body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8)); .body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
String errorMessage = buildErrorMessage(e, originalFilename); String errorMessage = buildErrorMessage(e, originalFilename);
log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e); log.error(
"EML to PDF conversion failed for {}: {}",
originalFilename,
errorMessage,
e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorMessage.getBytes(StandardCharsets.UTF_8)); .body(errorMessage.getBytes(StandardCharsets.UTF_8));
} catch (RuntimeException e) { } catch (RuntimeException e) {
String errorMessage = buildErrorMessage(e, originalFilename); String errorMessage = buildErrorMessage(e, originalFilename);
log.error("EML to PDF conversion failed for {}: {}", originalFilename, errorMessage, e); log.error(
"EML to PDF conversion failed for {}: {}",
originalFilename,
errorMessage,
e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorMessage.getBytes(StandardCharsets.UTF_8)); .body(errorMessage.getBytes(StandardCharsets.UTF_8));
} }

View File

@ -23,9 +23,9 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.api.GeneralFile;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;
import stirling.software.common.util.FileToPdf; import stirling.software.common.util.FileToPdf;
import stirling.software.common.util.WebResponseUtils; import stirling.software.common.util.WebResponseUtils;

View File

@ -1,8 +1,5 @@
package stirling.software.SPDF.controller.api.misc; package stirling.software.SPDF.controller.api.misc;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -20,17 +17,14 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter; import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam; import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
@ -44,6 +38,17 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory; import stirling.software.common.service.CustomPDFDocumentFactory;

View File

@ -36,8 +36,8 @@ import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.model.PipelineResult;
import stirling.software.SPDF.service.ApiDocService; import stirling.software.SPDF.service.ApiDocService;
import stirling.software.common.service.PostHogService;
import stirling.software.common.configuration.RuntimePathConfig; import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.service.PostHogService;
import stirling.software.common.util.FileMonitor; import stirling.software.common.util.FileMonitor;
@Service @Service

View File

@ -184,7 +184,8 @@ public class RedactController {
String pageNumbersInput = request.getPageNumbers(); String pageNumbersInput = request.getPageNumbers();
String[] parsedPageNumbers = String[] parsedPageNumbers =
pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0]; pageNumbersInput != null ? pageNumbersInput.split(",") : new String[0];
List<Integer> pageNumbers = GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false); List<Integer> pageNumbers =
GeneralUtils.parsePageList(parsedPageNumbers, pagesCount, false);
Collections.sort(pageNumbers); Collections.sort(pageNumbers);
return pageNumbers; return pageNumbers;
} }

View File

@ -7,6 +7,7 @@ import org.springframework.web.servlet.ModelAndView;
import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.common.util.CheckProgramInstall; import stirling.software.common.util.CheckProgramInstall;
@Controller @Controller

View File

@ -130,6 +130,13 @@ public class GeneralWebController {
return "view-pdf"; return "view-pdf";
} }
@GetMapping("/edit-table-of-contents")
@Hidden
public String editTableOfContents(Model model) {
model.addAttribute("currentPage", "edit-table-of-contents");
return "edit-table-of-contents";
}
@GetMapping("/multi-tool") @GetMapping("/multi-tool")
@Hidden @Hidden
public String multiToolForm(Model model) { public String multiToolForm(Model model) {

View File

@ -22,8 +22,8 @@ import io.swagger.v3.oas.annotations.Hidden;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
import stirling.software.SPDF.model.Dependency; import stirling.software.SPDF.model.Dependency;
import stirling.software.common.model.ApplicationProperties;
@Slf4j @Slf4j
@Controller @Controller
@ -48,9 +48,7 @@ public class HomeWebController {
InputStream is = resource.getInputStream(); InputStream is = resource.getInputStream();
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
Map<String, List<Dependency>> data = Map<String, List<Dependency>> data = mapper.readValue(json, new TypeReference<>() {});
mapper.readValue(json, new TypeReference<>() {
});
model.addAttribute("dependencies", data.get("dependencies")); model.addAttribute("dependencies", data.get("dependencies"));
} catch (IOException e) { } catch (IOException e) {
log.error("exception", e); log.error("exception", e);

View File

@ -0,0 +1,24 @@
package stirling.software.SPDF.model.api;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile;
@Data
@EqualsAndHashCode(callSuper = false)
public class EditTableOfContentsRequest extends PDFFile {
@Schema(
description = "Bookmark structure in JSON format",
example =
"[{\"title\":\"Chapter 1\",\"pageNumber\":1,\"children\":[{\"title\":\"Section 1.1\",\"pageNumber\":2}]}]")
private String bookmarkData;
@Schema(
description = "Whether to replace existing bookmarks or append to them",
example = "true")
private Boolean replaceExisting;
}

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.api.PDFFile;
@Data @Data

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.api.PDFFile;
@Data @Data

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
@Data @Data
@ -11,31 +12,31 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
public class ConvertToImageRequest extends PDFWithPageNums { public class ConvertToImageRequest extends PDFWithPageNums {
@Schema( @Schema(
description = "The output image format", description = "The output image format",
defaultValue = "png", defaultValue = "png",
allowableValues = {"png", "jpeg", "jpg", "gif", "webp"}, allowableValues = {"png", "jpeg", "jpg", "gif", "webp"},
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String imageFormat; private String imageFormat;
@Schema( @Schema(
description = description =
"Choose between a single image containing all pages or separate images for each" "Choose between a single image containing all pages or separate images for each"
+ " page", + " page",
defaultValue = "multiple", defaultValue = "multiple",
allowableValues = {"single", "multiple"}, allowableValues = {"single", "multiple"},
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String singleOrMultiple; private String singleOrMultiple;
@Schema( @Schema(
description = "The color type of the output image(s)", description = "The color type of the output image(s)",
defaultValue = "color", defaultValue = "color",
allowableValues = {"color", "greyscale", "blackwhite"}, allowableValues = {"color", "greyscale", "blackwhite"},
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String colorType; private String colorType;
@Schema( @Schema(
description = "The DPI (dots per inch) for the output image(s)", description = "The DPI (dots per inch) for the output image(s)",
defaultValue = "300", defaultValue = "300",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Integer dpi; private Integer dpi;
} }

View File

@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
public class ContainsTextRequest extends PDFWithPageNums { public class ContainsTextRequest extends PDFWithPageNums {
@Schema( @Schema(
description = "The text to check for", description = "The text to check for",
defaultValue = "text", defaultValue = "text",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String text; private String text;
} }

View File

@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison;
public class FileSizeRequest extends PDFComparison { public class FileSizeRequest extends PDFComparison {
@Schema( @Schema(
description = "Size of the file in bytes", description = "Size of the file in bytes",
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
defaultValue = "0") defaultValue = "0")
private long fileSize; private long fileSize;
} }

View File

@ -12,8 +12,8 @@ import stirling.software.SPDF.model.api.PDFComparison;
public class PageRotationRequest extends PDFComparison { public class PageRotationRequest extends PDFComparison {
@Schema( @Schema(
description = "Rotation in degrees", description = "Rotation in degrees",
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
defaultValue = "0") defaultValue = "0")
private int rotation; private int rotation;
} }

View File

@ -12,9 +12,9 @@ import stirling.software.SPDF.model.api.PDFComparison;
public class PageSizeRequest extends PDFComparison { public class PageSizeRequest extends PDFComparison {
@Schema( @Schema(
description = "Standard Page Size", description = "Standard Page Size",
allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"}, allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"},
defaultValue = "A4", defaultValue = "A4",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String standardPageSize; private String standardPageSize;
} }

View File

@ -32,4 +32,11 @@ public class MergePdfsRequest extends MultiplePDFFiles {
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
defaultValue = "true") defaultValue = "true")
private Boolean removeCertSign; private Boolean removeCertSign;
@Schema(
description =
"Flag indicating whether to generate a table of contents for the merged PDF. If true, a table of contents will be created using the input filenames as chapter names.",
requiredMode = Schema.RequiredMode.NOT_REQUIRED,
defaultValue = "false")
private boolean generateToc = false;
} }

View File

@ -14,33 +14,33 @@ import stirling.software.common.model.api.PDFFile;
public class OverlayPdfsRequest extends PDFFile { public class OverlayPdfsRequest extends PDFFile {
@Schema( @Schema(
description = description =
"An array of PDF files to be used as overlays on the base PDF. The order in" "An array of PDF files to be used as overlays on the base PDF. The order in"
+ " these files is applied based on the selected mode.", + " these files is applied based on the selected mode.",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile[] overlayFiles; private MultipartFile[] overlayFiles;
@Schema( @Schema(
description = description =
"The mode of overlaying: 'SequentialOverlay' for sequential application," "The mode of overlaying: 'SequentialOverlay' for sequential application,"
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'" + " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
+ " for fixed repetition based on provided counts", + " for fixed repetition based on provided counts",
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"}, allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String overlayMode; private String overlayMode;
@Schema( @Schema(
description = description =
"An array of integers specifying the number of times each corresponding overlay" "An array of integers specifying the number of times each corresponding overlay"
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should" + " file should be applied in the 'FixedRepeatOverlay' mode. This should"
+ " match the length of the overlayFiles array.", + " match the length of the overlayFiles array.",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private int[] counts; private int[] counts;
@Schema( @Schema(
description = "Overlay position 0 is Foregound, 1 is Background", description = "Overlay position 0 is Foregound, 1 is Background",
allowableValues = {"0", "1"}, allowableValues = {"0", "1"},
requiredMode = Schema.RequiredMode.REQUIRED, requiredMode = Schema.RequiredMode.REQUIRED,
type = "number") type = "number")
private int overlayPosition; private int overlayPosition;
} }

View File

@ -14,9 +14,9 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
public class AddStampRequest extends PDFWithPageNums { public class AddStampRequest extends PDFWithPageNums {
@Schema( @Schema(
description = "The stamp type (text or image)", description = "The stamp type (text or image)",
allowableValues = {"text", "image"}, allowableValues = {"text", "image"},
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String stampType; private String stampType;
@Schema(description = "The stamp text", defaultValue = "Stirling Software") @Schema(description = "The stamp text", defaultValue = "Stirling Software")
@ -26,60 +26,60 @@ public class AddStampRequest extends PDFWithPageNums {
private MultipartFile stampImage; private MultipartFile stampImage;
@Schema( @Schema(
description = "The selected alphabet of the stamp text", description = "The selected alphabet of the stamp text",
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
defaultValue = "roman") defaultValue = "roman")
private String alphabet = "roman"; private String alphabet = "roman";
@Schema( @Schema(
description = "The font size of the stamp text and image", description = "The font size of the stamp text and image",
defaultValue = "30", defaultValue = "30",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float fontSize; private float fontSize;
@Schema( @Schema(
description = "The rotation of the stamp in degrees", description = "The rotation of the stamp in degrees",
defaultValue = "0", defaultValue = "0",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float rotation; private float rotation;
@Schema( @Schema(
description = "The opacity of the stamp (0.0 - 1.0)", description = "The opacity of the stamp (0.0 - 1.0)",
defaultValue = "0.5", defaultValue = "0.5",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float opacity; private float opacity;
@Schema( @Schema(
description = description =
"Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center," "Position for stamp placement based on a 1-9 grid (1: bottom-left, 2: bottom-center,"
+ " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right," + " 3: bottom-right, 4: middle-left, 5: middle-center, 6: middle-right,"
+ " 7: top-left, 8: top-center, 9: top-right)", + " 7: top-left, 8: top-center, 9: top-right)",
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}, allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
defaultValue = "5", defaultValue = "5",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private int position; private int position;
@Schema( @Schema(
description = description =
"Override X coordinate for stamp placement. If set, it will override the" "Override X coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.", + " position-based calculation. Negative value means no override.",
defaultValue = "-1", defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float overrideX; // Default to -1 indicating no override private float overrideX; // Default to -1 indicating no override
@Schema( @Schema(
description = description =
"Override Y coordinate for stamp placement. If set, it will override the" "Override Y coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.", + " position-based calculation. Negative value means no override.",
defaultValue = "-1", defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float overrideY; // Default to -1 indicating no override private float overrideY; // Default to -1 indicating no override
@Schema( @Schema(
description = "Specifies the margin size for the stamp.", description = "Specifies the margin size for the stamp.",
allowableValues = {"small", "medium", "large", "x-large"}, allowableValues = {"small", "medium", "large", "x-large"},
defaultValue = "medium", defaultValue = "medium",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String customMargin; private String customMargin;
@Schema(description = "The color of the stamp text", defaultValue = "#d3d3d3") @Schema(description = "The color of the stamp text", defaultValue = "#d3d3d3")

View File

@ -14,71 +14,71 @@ import stirling.software.common.model.api.PDFFile;
public class MetadataRequest extends PDFFile { public class MetadataRequest extends PDFFile {
@Schema( @Schema(
description = "Delete all metadata if set to true", description = "Delete all metadata if set to true",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean deleteAll; private Boolean deleteAll;
@Schema( @Schema(
description = "The author of the document", description = "The author of the document",
defaultValue = "author", defaultValue = "author",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String author; private String author;
@Schema( @Schema(
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)", description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss", pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00", defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String creationDate; private String creationDate;
@Schema( @Schema(
description = "The creator of the document", description = "The creator of the document",
defaultValue = "creator", defaultValue = "creator",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String creator; private String creator;
@Schema( @Schema(
description = "The keywords for the document", description = "The keywords for the document",
defaultValue = "keywords", defaultValue = "keywords",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String keywords; private String keywords;
@Schema( @Schema(
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)", description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss", pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00", defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String modificationDate; private String modificationDate;
@Schema( @Schema(
description = "The producer of the document", description = "The producer of the document",
defaultValue = "producer", defaultValue = "producer",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String producer; private String producer;
@Schema( @Schema(
description = "The subject of the document", description = "The subject of the document",
defaultValue = "subject", defaultValue = "subject",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String subject; private String subject;
@Schema( @Schema(
description = "The title of the document", description = "The title of the document",
defaultValue = "title", defaultValue = "title",
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String title; private String title;
@Schema( @Schema(
description = "The trapped status of the document", description = "The trapped status of the document",
defaultValue = "False", defaultValue = "False",
allowableValues = {"True", "False", "Unknown"}, allowableValues = {"True", "False", "Unknown"},
requiredMode = Schema.RequiredMode.NOT_REQUIRED) requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String trapped; private String trapped;
@Schema( @Schema(
description = description =
"Map list of key and value of custom parameters. Note these must start with" "Map list of key and value of custom parameters. Note these must start with"
+ " customKey and customValue if they are non-standard") + " customKey and customValue if they are non-standard")
private Map<String, String> allRequestParams; private Map<String, String> allRequestParams;
} }

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.api.PDFFile;
@Data @Data

View File

@ -12,24 +12,24 @@ import stirling.software.common.model.api.PDFFile;
public class AddPasswordRequest extends PDFFile { public class AddPasswordRequest extends PDFFile {
@Schema( @Schema(
description = description =
"The owner password to be added to the PDF file (Restricts what can be done" "The owner password to be added to the PDF file (Restricts what can be done"
+ " with the document once it is opened)", + " with the document once it is opened)",
format = "password") format = "password")
private String ownerPassword; private String ownerPassword;
@Schema( @Schema(
description = description =
"The password to be added to the PDF file (Restricts the opening of the" "The password to be added to the PDF file (Restricts the opening of the"
+ " document itself.)", + " document itself.)",
format = "password") format = "password")
private String password; private String password;
@Schema( @Schema(
description = "The length of the encryption key", description = "The length of the encryption key",
allowableValues = {"40", "128", "256"}, allowableValues = {"40", "128", "256"},
defaultValue = "256", defaultValue = "256",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private int keyLength = 256; private int keyLength = 256;
@Schema(description = "Whether document assembly is prevented", defaultValue = "false") @Schema(description = "Whether document assembly is prevented", defaultValue = "false")
@ -39,8 +39,8 @@ public class AddPasswordRequest extends PDFFile {
private Boolean preventExtractContent; private Boolean preventExtractContent;
@Schema( @Schema(
description = "Whether content extraction for accessibility is prevented", description = "Whether content extraction for accessibility is prevented",
defaultValue = "false") defaultValue = "false")
private Boolean preventExtractForAccessibility; private Boolean preventExtractForAccessibility;
@Schema(description = "Whether form filling is prevented", defaultValue = "false") @Schema(description = "Whether form filling is prevented", defaultValue = "false")
@ -50,8 +50,8 @@ public class AddPasswordRequest extends PDFFile {
private Boolean preventModify; private Boolean preventModify;
@Schema( @Schema(
description = "Whether modification of annotations is prevented", description = "Whether modification of annotations is prevented",
defaultValue = "false") defaultValue = "false")
private Boolean preventModifyAnnotations; private Boolean preventModifyAnnotations;
@Schema(description = "Whether printing of the document is prevented", defaultValue = "false") @Schema(description = "Whether printing of the document is prevented", defaultValue = "false")

View File

@ -14,19 +14,19 @@ import stirling.software.common.model.api.security.RedactionArea;
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
public class ManualRedactPdfRequest extends PDFWithPageNums { public class ManualRedactPdfRequest extends PDFWithPageNums {
@Schema( @Schema(
description = "A list of areas that should be redacted", description = "A list of areas that should be redacted",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private List<RedactionArea> redactions; private List<RedactionArea> redactions;
@Schema( @Schema(
description = "Convert the redacted PDF to an image", description = "Convert the redacted PDF to an image",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean convertPDFToImage; private Boolean convertPDFToImage;
@Schema( @Schema(
description = "The color used to fully redact certain pages", description = "The color used to fully redact certain pages",
defaultValue = "#000000", defaultValue = "#000000",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String pageRedactionColor; private String pageRedactionColor;
} }

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import stirling.software.common.model.api.PDFFile; import stirling.software.common.model.api.PDFFile;
@Data @Data
@ -11,38 +12,38 @@ import stirling.software.common.model.api.PDFFile;
public class RedactPdfRequest extends PDFFile { public class RedactPdfRequest extends PDFFile {
@Schema( @Schema(
description = "List of text to redact from the PDF", description = "List of text to redact from the PDF",
defaultValue = "text,text2", defaultValue = "text,text2",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String listOfText; private String listOfText;
@Schema( @Schema(
description = "Whether to use regex for the listOfText", description = "Whether to use regex for the listOfText",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean useRegex; private Boolean useRegex;
@Schema( @Schema(
description = "Whether to use whole word search", description = "Whether to use whole word search",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean wholeWordSearch; private Boolean wholeWordSearch;
@Schema( @Schema(
description = "The color for redaction", description = "The color for redaction",
defaultValue = "#000000", defaultValue = "#000000",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private String redactColor; private String redactColor;
@Schema( @Schema(
description = "Custom padding for redaction", description = "Custom padding for redaction",
type = "number", type = "number",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private float customPadding; private float customPadding;
@Schema( @Schema(
description = "Convert the redacted PDF to an image", description = "Convert the redacted PDF to an image",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean convertPDFToImage; private Boolean convertPDFToImage;
} }

View File

@ -12,38 +12,38 @@ import stirling.software.common.model.api.PDFFile;
public class SanitizePdfRequest extends PDFFile { public class SanitizePdfRequest extends PDFFile {
@Schema( @Schema(
description = "Remove JavaScript actions from the PDF", description = "Remove JavaScript actions from the PDF",
defaultValue = "true", defaultValue = "true",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeJavaScript; private Boolean removeJavaScript;
@Schema( @Schema(
description = "Remove embedded files from the PDF", description = "Remove embedded files from the PDF",
defaultValue = "true", defaultValue = "true",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeEmbeddedFiles; private Boolean removeEmbeddedFiles;
@Schema( @Schema(
description = "Remove XMP metadata from the PDF", description = "Remove XMP metadata from the PDF",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeXMPMetadata; private Boolean removeXMPMetadata;
@Schema( @Schema(
description = "Remove document info metadata from the PDF", description = "Remove document info metadata from the PDF",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeMetadata; private Boolean removeMetadata;
@Schema( @Schema(
description = "Remove links from the PDF", description = "Remove links from the PDF",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeLinks; private Boolean removeLinks;
@Schema( @Schema(
description = "Remove fonts from the PDF", description = "Remove fonts from the PDF",
defaultValue = "false", defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED) requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean removeFonts; private Boolean removeFonts;
} }

View File

@ -14,8 +14,8 @@ import io.micrometer.core.instrument.search.Search;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import stirling.software.common.service.PostHogService;
import stirling.software.SPDF.config.EndpointInspector; import stirling.software.SPDF.config.EndpointInspector;
import stirling.software.common.service.PostHogService;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor

View File

@ -29,13 +29,11 @@ spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json spring.web.resources.mime-mappings.webmanifest=application/manifest+json
spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000} spring.mvc.async.request-timeout=${SYSTEM_CONNECTIONTIMEOUTMILLISECONDS:1200000}
management.endpoints.web.exposure.include=beans
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL spring.datasource.url=jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL
spring.datasource.driver-class-name=org.h2.Driver spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa spring.datasource.username=sa
spring.datasource.password= spring.datasource.password=
spring.h2.console.enabled=true spring.h2.console.enabled=false
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
server.servlet.session.timeout:30m server.servlet.session.timeout:30m
# Change the default URL path for OpenAPI JSON # Change the default URL path for OpenAPI JSON
@ -45,3 +43,5 @@ springdoc.swagger-ui.url=/v1/api-docs
springdoc.swagger-ui.path=/index.html springdoc.swagger-ui.path=/index.html
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
posthog.host=https://eu.i.posthog.com posthog.host=https://eu.i.posthog.com
spring.main.allow-bean-definition-overriding=true

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,138 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amharic
lang.ara=Arabic
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (Cyrillic)
lang.bel=Belarusian
lang.ben=Bengali
lang.bod=Tibetan
lang.bos=Bosnian
lang.bre=Breton
lang.bul=Bulgarian
lang.cat=Catalan
lang.ceb=Cebuano
lang.ces=Czech
lang.chi_sim=Chinese (Simplified)
lang.chi_sim_vert=Chinese (Simplified, Vertical)
lang.chi_tra=Chinese (Traditional)
lang.chi_tra_vert=Chinese (Traditional, Vertical)
lang.chr=Cherokee
lang.cos=Corsican
lang.cym=Welsh
lang.dan=Danish
lang.dan_frak=Danish (Fraktur)
lang.deu=German
lang.deu_frak=German (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greek
lang.eng=English
lang.enm=English, Middle (1100-1500)
lang.epo=Esperanto
lang.equ=Math / equation detection module
lang.est=Estonian
lang.eus=Basque
lang.fao=Faroese
lang.fas=Persian
lang.fil=Filipino
lang.fin=Finnish
lang.fra=French
lang.frk=Frankish
lang.frm=French, Middle (ca.1400-1600)
lang.fry=Western Frisian
lang.gla=Scottish Gaelic
lang.gle=Irish
lang.glg=Galician
lang.grc=Ancient Greek
lang.guj=Gujarati
lang.hat=Haitian, Haitian Creole
lang.heb=Hebrew
lang.hin=Hindi
lang.hrv=Croatian
lang.hun=Hungarian
lang.hye=Armenian
lang.iku=Inuktitut
lang.ind=Indonesian
lang.isl=Icelandic
lang.ita=Italian
lang.ita_old=Italian (Old)
lang.jav=Javanese
lang.jpn=Japanese
lang.jpn_vert=Japanese (Vertical)
lang.kan=Kannada
lang.kat=Georgian
lang.kat_old=Georgian (Old)
lang.kaz=Kazakh
lang.khm=Central Khmer
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Northern Kurdish
lang.kor=Korean
lang.kor_vert=Korean (Vertical)
lang.lao=Lao
lang.lat=Latin
lang.lav=Latvian
lang.lit=Lithuanian
lang.ltz=Luxembourgish
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedonian
lang.mlt=Maltese
lang.mon=Mongolian
lang.mri=Maori
lang.msa=Malay
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Dutch; Flemish
lang.nor=Norwegian
lang.oci=Occitan (post 1500)
lang.ori=Oriya
lang.osd=Orientation and script detection module
lang.pan=Panjabi, Punjabi
lang.pol=Polish
lang.por=Portuguese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Romanian, Moldavian, Moldovan
lang.rus=Russian
lang.san=Sanskrit
lang.sin=Sinhala, Sinhalese
lang.slk=Slovak
lang.slk_frak=Slovak (Fraktur)
lang.slv=Slovenian
lang.snd=Sindhi
lang.spa=Spanish
lang.spa_old=Spanish (Old)
lang.sqi=Albanian
lang.srp=Serbian
lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Swedish
lang.syr=Syriac
lang.tam=Tamil
lang.tat=Tatar
lang.tel=Telugu
lang.tgk=Tajik
lang.tgl=Tagalog
lang.tha=Thai
lang.tir=Tigrinya
lang.ton=Tonga (Tonga Islands)
lang.tur=Turkish
lang.uig=Uighur, Uyghur
lang.ukr=Ukrainian
lang.urd=Urdu
lang.uzb=Uzbek
lang.uzb_cyrl=Uzbek (Cyrillic)
lang.vie=Vietnamese
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Font Size addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name addPageNumbers.fontName=Font Name
pdfPrompt=Select PDF(s) pdfPrompt=Select PDF(s)
@ -87,6 +219,12 @@ addToDoc=Add to Document
reset=Reset reset=Reset
apply=Apply apply=Apply
noFileSelected=No file selected. Please upload one. noFileSelected=No file selected. Please upload one.
view=View
cancel=Cancel
back.toSettings=Back to Settings
back.toHome=Back to Home
back.toAdmin=Back to Admin
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions
@ -127,6 +265,7 @@ enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users. enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher
################# #################
@ -207,6 +346,8 @@ account.property=Property
account.webBrowserSettings=Web Browser Setting account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=Sync Account -> Browser account.syncToBrowser=Sync Account -> Browser
account.syncToAccount=Sync Account <- Browser account.syncToAccount=Sync Account <- Browser
account.adminTitle=Administrator Tools
account.adminNotif=You have admin privileges. Access system settings and user management.
adminUserSettings.title=User Control Settings adminUserSettings.title=User Control Settings
@ -238,6 +379,39 @@ adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users: adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request adminUserSettings.lastRequest=Last Request
adminUserSettings.usage=View Usage adminUserSettings.usage=View Usage
adminUserSettings.teams=View/Edit Teams
adminUserSettings.team=Team
adminUserSettings.manageTeams=Manage Teams
adminUserSettings.createTeam=Create Team
adminUserSettings.viewTeam=View Team
adminUserSettings.deleteTeam=Delete Team
adminUserSettings.teamName=Team Name
adminUserSettings.teamExists=Team already exists
adminUserSettings.teamCreated=Team created successfully
adminUserSettings.teamChanged=User's team was updated
adminUserSettings.totalMembers=Total Members
adminUserSettings.confirmDeleteTeam=Are you sure you want to delete this team?
teamCreated=Team created successfully
teamExists=A team with that name already exists
teamNameExists=Another team with that name already exists
teamNotFound=Team not found
teamDeleted=Team deleted
teamHasUsers=Cannot delete a team with users assigned
teamRenamed=Team renamed successfully
# Team user management
team.addUser=Add User to Team
team.selectUser=Select User
team.warning.moveUser=Warning: This will move the user from "{0}" team to "{1}" team. Are you sure?
team.confirm.moveUser=Are you sure you want to move this user from "{0}" team to "{1}" team?
team.userAdded=User successfully added to team
team.back=Back to Teams
team.internal=Internal Team
team.internalTeamNotAccessible=The Internal team is a system team and cannot be accessed
team.cannotMoveInternalUsers=Users in the Internal team cannot be moved to other teams
endpointStatistics.title=Endpoint Statistics endpointStatistics.title=Endpoint Statistics
endpointStatistics.header=Endpoint Statistics endpointStatistics.header=Endpoint Statistics
@ -1026,6 +1200,7 @@ merge.header=Merge multiple PDFs (2+)
merge.sortByName=Sort by name merge.sortByName=Sort by name
merge.sortByDate=Sort by date merge.sortByDate=Sort by date
merge.removeCertSign=Remove digital signature in the merged file? merge.removeCertSign=Remove digital signature in the merged file?
merge.generateToc=Generate table of contents in the merged file?
merge.submit=Merge merge.submit=Merge
@ -1475,3 +1650,57 @@ cookieBanner.preferencesModal.necessary.description=These cookies are essential
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)
# Table of Contents Feature
home.editTableOfContents.title=Edit Table of Contents
home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents
editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline
editTableOfContents.title=Edit Table of Contents
editTableOfContents.header=Add or Edit PDF Table of Contents
editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing)
editTableOfContents.editorTitle=Bookmark Editor
editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks.
editTableOfContents.addBookmark=Add New Bookmark
editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document.
editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks.
editTableOfContents.desc.3=Each bookmark requires a title and target page number.
editTableOfContents.submit=Apply Table of Contents

View File

@ -3,6 +3,138 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amharic
lang.ara=Arabic
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (Cyrillic)
lang.bel=Belarusian
lang.ben=Bengali
lang.bod=Tibetan
lang.bos=Bosnian
lang.bre=Breton
lang.bul=Bulgarian
lang.cat=Catalan
lang.ceb=Cebuano
lang.ces=Czech
lang.chi_sim=Chinese (Simplified)
lang.chi_sim_vert=Chinese (Simplified, Vertical)
lang.chi_tra=Chinese (Traditional)
lang.chi_tra_vert=Chinese (Traditional, Vertical)
lang.chr=Cherokee
lang.cos=Corsican
lang.cym=Welsh
lang.dan=Danish
lang.dan_frak=Danish (Fraktur)
lang.deu=German
lang.deu_frak=German (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greek
lang.eng=English
lang.enm=English, Middle (1100-1500)
lang.epo=Esperanto
lang.equ=Math / equation detection module
lang.est=Estonian
lang.eus=Basque
lang.fao=Faroese
lang.fas=Persian
lang.fil=Filipino
lang.fin=Finnish
lang.fra=French
lang.frk=Frankish
lang.frm=French, Middle (ca.1400-1600)
lang.fry=Western Frisian
lang.gla=Scottish Gaelic
lang.gle=Irish
lang.glg=Galician
lang.grc=Ancient Greek
lang.guj=Gujarati
lang.hat=Haitian, Haitian Creole
lang.heb=Hebrew
lang.hin=Hindi
lang.hrv=Croatian
lang.hun=Hungarian
lang.hye=Armenian
lang.iku=Inuktitut
lang.ind=Indonesian
lang.isl=Icelandic
lang.ita=Italian
lang.ita_old=Italian (Old)
lang.jav=Javanese
lang.jpn=Japanese
lang.jpn_vert=Japanese (Vertical)
lang.kan=Kannada
lang.kat=Georgian
lang.kat_old=Georgian (Old)
lang.kaz=Kazakh
lang.khm=Central Khmer
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Northern Kurdish
lang.kor=Korean
lang.kor_vert=Korean (Vertical)
lang.lao=Lao
lang.lat=Latin
lang.lav=Latvian
lang.lit=Lithuanian
lang.ltz=Luxembourgish
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedonian
lang.mlt=Maltese
lang.mon=Mongolian
lang.mri=Maori
lang.msa=Malay
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Dutch; Flemish
lang.nor=Norwegian
lang.oci=Occitan (post 1500)
lang.ori=Oriya
lang.osd=Orientation and script detection module
lang.pan=Panjabi, Punjabi
lang.pol=Polish
lang.por=Portuguese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Romanian, Moldavian, Moldovan
lang.rus=Russian
lang.san=Sanskrit
lang.sin=Sinhala, Sinhalese
lang.slk=Slovak
lang.slk_frak=Slovak (Fraktur)
lang.slv=Slovenian
lang.snd=Sindhi
lang.spa=Spanish
lang.spa_old=Spanish (Old)
lang.sqi=Albanian
lang.srp=Serbian
lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Swedish
lang.syr=Syriac
lang.tam=Tamil
lang.tat=Tatar
lang.tel=Telugu
lang.tgk=Tajik
lang.tgl=Tagalog
lang.tha=Thai
lang.tir=Tigrinya
lang.ton=Tonga (Tonga Islands)
lang.tur=Turkish
lang.uig=Uighur, Uyghur
lang.ukr=Ukrainian
lang.urd=Urdu
lang.uzb=Uzbek
lang.uzb_cyrl=Uzbek (Cyrillic)
lang.vie=Vietnamese
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Font Size addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name addPageNumbers.fontName=Font Name
pdfPrompt=Select PDF(s) pdfPrompt=Select PDF(s)
@ -127,6 +259,7 @@ enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users. enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features. enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro license or higher
################# #################
@ -1380,14 +1513,13 @@ error.copyStack=Copy Stack Trace
error.githubSubmit=GitHub - Submit a ticket error.githubSubmit=GitHub - Submit a ticket
error.discordSubmit=Discord - Submit Support post error.discordSubmit=Discord - Submit Support post
#remove-image #remove-image
removeImage.title=Remove image removeImage.title=Remove image
removeImage.header=Remove image removeImage.header=Remove image
removeImage.removeImage=Remove image removeImage.removeImage=Remove image
removeImage.submit=Remove image removeImage.submit=Remove image
#split-by-chapters
splitByChapters.title=Split PDF by Chapters splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level splitByChapters.bookmarkLevel=Bookmark Level
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner # # Cookie banner #
#################### ####################
cookieBanner.popUp.title=How we use Cookies cookieBanner.popUp.title=How we use Cookies
cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for youhelping us improve our tools and keep building features you'll love. cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you\u2014helping us improve our tools and keep building features you'll love.
cookieBanner.popUp.description.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly. cookieBanner.popUp.description.2=If you\u2019d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.acceptAllBtn=Okay cookieBanner.popUp.acceptAllBtn=Okay
cookieBanner.popUp.acceptNecessaryBtn=No Thanks cookieBanner.popUp.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
cookieBanner.preferencesModal.subtitle=Cookie Usage cookieBanner.preferencesModal.subtitle=Cookie Usage
cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users. cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.
cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never—track or access the content of the documents you use. cookieBanner.preferencesModal.description.2=Stirling PDF cannot\u2014and will never\u2014track or access the content of the documents you use.
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off. cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms\u2014which is why they can\u2019t be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assuredStirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured\u2014Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,138 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amharic
lang.ara=Arabic
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (Cyrillic)
lang.bel=Belarusian
lang.ben=Bengali
lang.bod=Tibetan
lang.bos=Bosnian
lang.bre=Breton
lang.bul=Bulgarian
lang.cat=Catalan
lang.ceb=Cebuano
lang.ces=Czech
lang.chi_sim=Chinese (Simplified)
lang.chi_sim_vert=Chinese (Simplified, Vertical)
lang.chi_tra=Chinese (Traditional)
lang.chi_tra_vert=Chinese (Traditional, Vertical)
lang.chr=Cherokee
lang.cos=Corsican
lang.cym=Welsh
lang.dan=Danish
lang.dan_frak=Danish (Fraktur)
lang.deu=German
lang.deu_frak=German (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greek
lang.eng=English
lang.enm=English, Middle (1100-1500)
lang.epo=Esperanto
lang.equ=Math / equation detection module
lang.est=Estonian
lang.eus=Basque
lang.fao=Faroese
lang.fas=Persian
lang.fil=Filipino
lang.fin=Finnish
lang.fra=French
lang.frk=Frankish
lang.frm=French, Middle (ca.1400-1600)
lang.fry=Western Frisian
lang.gla=Scottish Gaelic
lang.gle=Irish
lang.glg=Galician
lang.grc=Ancient Greek
lang.guj=Gujarati
lang.hat=Haitian, Haitian Creole
lang.heb=Hebrew
lang.hin=Hindi
lang.hrv=Croatian
lang.hun=Hungarian
lang.hye=Armenian
lang.iku=Inuktitut
lang.ind=Indonesian
lang.isl=Icelandic
lang.ita=Italian
lang.ita_old=Italian (Old)
lang.jav=Javanese
lang.jpn=Japanese
lang.jpn_vert=Japanese (Vertical)
lang.kan=Kannada
lang.kat=Georgian
lang.kat_old=Georgian (Old)
lang.kaz=Kazakh
lang.khm=Central Khmer
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Northern Kurdish
lang.kor=Korean
lang.kor_vert=Korean (Vertical)
lang.lao=Lao
lang.lat=Latin
lang.lav=Latvian
lang.lit=Lithuanian
lang.ltz=Luxembourgish
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedonian
lang.mlt=Maltese
lang.mon=Mongolian
lang.mri=Maori
lang.msa=Malay
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Dutch; Flemish
lang.nor=Norwegian
lang.oci=Occitan (post 1500)
lang.ori=Oriya
lang.osd=Orientation and script detection module
lang.pan=Panjabi, Punjabi
lang.pol=Polish
lang.por=Portuguese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Romanian, Moldavian, Moldovan
lang.rus=Russian
lang.san=Sanskrit
lang.sin=Sinhala, Sinhalese
lang.slk=Slovak
lang.slk_frak=Slovak (Fraktur)
lang.slv=Slovenian
lang.snd=Sindhi
lang.spa=Spanish
lang.spa_old=Spanish (Old)
lang.sqi=Albanian
lang.srp=Serbian
lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Swedish
lang.syr=Syriac
lang.tam=Tamil
lang.tat=Tatar
lang.tel=Telugu
lang.tgk=Tajik
lang.tgl=Tagalog
lang.tha=Thai
lang.tir=Tigrinya
lang.ton=Tonga (Tonga Islands)
lang.tur=Turkish
lang.uig=Uighur, Uyghur
lang.ukr=Ukrainian
lang.urd=Urdu
lang.uzb=Uzbek
lang.uzb_cyrl=Uzbek (Cyrillic)
lang.vie=Vietnamese
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Font Size addPageNumbers.fontSize=Font Size
addPageNumbers.fontName=Font Name addPageNumbers.fontName=Font Name
pdfPrompt=Hautatu PDFa(k) pdfPrompt=Hautatu PDFa(k)
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner # # Cookie banner #
#################### ####################
cookieBanner.popUp.title=How we use Cookies cookieBanner.popUp.title=How we use Cookies
cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for youhelping us improve our tools and keep building features you'll love. cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you?helping us improve our tools and keep building features you'll love.
cookieBanner.popUp.description.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly. cookieBanner.popUp.description.2=If you?d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.acceptAllBtn=Okay cookieBanner.popUp.acceptAllBtn=Okay
cookieBanner.popUp.acceptNecessaryBtn=No Thanks cookieBanner.popUp.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
cookieBanner.preferencesModal.subtitle=Cookie Usage cookieBanner.preferencesModal.subtitle=Cookie Usage
cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users. cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.
cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never—track or access the content of the documents you use. cookieBanner.preferencesModal.description.2=Stirling PDF cannot?and will never?track or access the content of the documents you use.
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off. cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms?which is why they can?t be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assuredStirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,138 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amharic
lang.ara=Arabic
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (Cyrillic)
lang.bel=Belarusian
lang.ben=Bengali
lang.bod=Tibetan
lang.bos=Bosnian
lang.bre=Breton
lang.bul=Bulgarian
lang.cat=Catalan
lang.ceb=Cebuano
lang.ces=Czech
lang.chi_sim=Chinese (Simplified)
lang.chi_sim_vert=Chinese (Simplified, Vertical)
lang.chi_tra=Chinese (Traditional)
lang.chi_tra_vert=Chinese (Traditional, Vertical)
lang.chr=Cherokee
lang.cos=Corsican
lang.cym=Welsh
lang.dan=Danish
lang.dan_frak=Danish (Fraktur)
lang.deu=German
lang.deu_frak=German (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greek
lang.eng=English
lang.enm=English, Middle (1100-1500)
lang.epo=Esperanto
lang.equ=Math / equation detection module
lang.est=Estonian
lang.eus=Basque
lang.fao=Faroese
lang.fas=Persian
lang.fil=Filipino
lang.fin=Finnish
lang.fra=French
lang.frk=Frankish
lang.frm=French, Middle (ca.1400-1600)
lang.fry=Western Frisian
lang.gla=Scottish Gaelic
lang.gle=Irish
lang.glg=Galician
lang.grc=Ancient Greek
lang.guj=Gujarati
lang.hat=Haitian, Haitian Creole
lang.heb=Hebrew
lang.hin=Hindi
lang.hrv=Croatian
lang.hun=Hungarian
lang.hye=Armenian
lang.iku=Inuktitut
lang.ind=Indonesian
lang.isl=Icelandic
lang.ita=Italian
lang.ita_old=Italian (Old)
lang.jav=Javanese
lang.jpn=Japanese
lang.jpn_vert=Japanese (Vertical)
lang.kan=Kannada
lang.kat=Georgian
lang.kat_old=Georgian (Old)
lang.kaz=Kazakh
lang.khm=Central Khmer
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Northern Kurdish
lang.kor=Korean
lang.kor_vert=Korean (Vertical)
lang.lao=Lao
lang.lat=Latin
lang.lav=Latvian
lang.lit=Lithuanian
lang.ltz=Luxembourgish
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedonian
lang.mlt=Maltese
lang.mon=Mongolian
lang.mri=Maori
lang.msa=Malay
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Dutch; Flemish
lang.nor=Norwegian
lang.oci=Occitan (post 1500)
lang.ori=Oriya
lang.osd=Orientation and script detection module
lang.pan=Panjabi, Punjabi
lang.pol=Polish
lang.por=Portuguese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Romanian, Moldavian, Moldovan
lang.rus=Russian
lang.san=Sanskrit
lang.sin=Sinhala, Sinhalese
lang.slk=Slovak
lang.slk_frak=Slovak (Fraktur)
lang.slv=Slovenian
lang.snd=Sindhi
lang.spa=Spanish
lang.spa_old=Spanish (Old)
lang.sqi=Albanian
lang.srp=Serbian
lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Swedish
lang.syr=Syriac
lang.tam=Tamil
lang.tat=Tatar
lang.tel=Telugu
lang.tgk=Tajik
lang.tgl=Tagalog
lang.tha=Thai
lang.tir=Tigrinya
lang.ton=Tonga (Tonga Islands)
lang.tur=Turkish
lang.uig=Uighur, Uyghur
lang.ukr=Ukrainian
lang.urd=Urdu
lang.uzb=Uzbek
lang.uzb_cyrl=Uzbek (Cyrillic)
lang.vie=Vietnamese
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Ukuran Fonta addPageNumbers.fontSize=Ukuran Fonta
addPageNumbers.fontName=Nama Fonta addPageNumbers.fontName=Nama Fonta
pdfPrompt=Pilih PDF pdfPrompt=Pilih PDF
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner # # Cookie banner #
#################### ####################
cookieBanner.popUp.title=How we use Cookies cookieBanner.popUp.title=How we use Cookies
cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for youhelping us improve our tools and keep building features you'll love. cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you?helping us improve our tools and keep building features you'll love.
cookieBanner.popUp.description.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly. cookieBanner.popUp.description.2=If you?d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.acceptAllBtn=Okay cookieBanner.popUp.acceptAllBtn=Okay
cookieBanner.popUp.acceptNecessaryBtn=No Thanks cookieBanner.popUp.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
cookieBanner.preferencesModal.subtitle=Cookie Usage cookieBanner.preferencesModal.subtitle=Cookie Usage
cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users. cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.
cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never—track or access the content of the documents you use. cookieBanner.preferencesModal.description.2=Stirling PDF cannot?and will never?track or access the content of the documents you use.
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off. cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms?which is why they can?t be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assuredStirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)

View File

@ -3,16 +3,148 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amarico
lang.ara=Arabo
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (cirillico)
lang.bel=Bielorusso
lang.ben=Bengalese
lang.bod=Tibetano
lang.bos=Bosniaco
lang.bre=Bretone
lang.bul=Bulgaro
lang.cat=Catalano
lang.ceb=Cebuano
lang.ces=Ceco
lang.chi_sim=Cinese (semplificato)
lang.chi_sim_vert=Cinese (semplificato, verticale)
lang.chi_tra=Cinese (Tradizionale)
lang.chi_tra_vert=Cinese (tradizionale, verticale)
lang.chr=Cherokee
lang.cos=Corso
lang.cym=Gallese
lang.dan=Danese
lang.dan_frak=Danese (Fraktur)
lang.deu=Tedesco
lang.deu_frak=Tedesco (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greco
lang.eng=Inglese
lang.enm=Inglese, Medio (1100-1500)
lang.epo=Esperanto
lang.equ=Modulo di rilevamento di equazioni/matematiche
lang.est=Estone
lang.eus=Basco
lang.fao=Faroese
lang.fas=Persiano
lang.fil=Filippino
lang.fin=Finlandese
lang.fra=Francese
lang.frk=Franco
lang.frm=Francese, Medio (ca.1400-1600)
lang.fry=Frisone occidentale
lang.gla=Gaelico scozzese
lang.gle=Irlandese
lang.glg=Galiziano
lang.grc=Greco antico
lang.guj=Gujarati
lang.hat=Haitiano, Creolo Haitiano
lang.heb=Ebraico
lang.hin=Hindi
lang.hrv=Croato
lang.hun=Ungherese
lang.hye=Armeno
lang.iku=Inuktitut
lang.ind=Indonesiano
lang.isl=Islandese
lang.ita=Italiano
lang.ita_old=Italiano (antico)
lang.jav=Giavanese
lang.jpn=Giapponese
lang.jpn_vert=Giapponese (verticale)
lang.kan=Kannada
lang.kat=Georgiano
lang.kat_old=Georgiano (antico)
lang.kaz=kazako
lang.khm=Khmer centrale
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Curdo settentrionale
lang.kor=Coreano
lang.kor_vert=Coreano (verticale)
lang.lao=Lao
lang.lat=Latino
lang.lav=Lettone
lang.lit=Lituano
lang.ltz=Lussemburghese
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedone
lang.mlt=Maltese
lang.mon=Mongola
lang.mri=Maori
lang.msa=Malase
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Olandese; Fiammingo
lang.nor=Norvegese
lang.oci=Occitano (post 1500)
lang.ori=Oriya
lang.osd=Modulo di orientamento e rilevamento degli script
lang.pan=Panjabi, Punjabi
lang.pol=Polacco
lang.por=Portoghese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Rumeno, Moldavo, Moldovan
lang.rus=Russo
lang.san=Sanscrito
lang.sin=Singala, Singalese
lang.slk=Slovacco
lang.slk_frak=Slovacco (Fraktur)
lang.slv=Sloveno
lang.snd=Sindhi
lang.spa=Spagnolo
lang.spa_old=Spagnolo (antico)
lang.sqi=Albanese
lang.srp=Serbo
lang.srp_latn=Serbo (latino)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Svedese
lang.syr=Sriaco
lang.tam=Tamil
lang.tat=Tataro
lang.tel=Telugu
lang.tgk=Tagiko
lang.tgl=Tagalog
lang.tha=Tailandese
lang.tir=Tigrino
lang.ton=Tonga (Isole Tonga)
lang.tur=Turco
lang.uig=Uighur, Uyghur
lang.ukr=Ucraino
lang.urd=Urdu
lang.uzb=Uzbeko
lang.uzb_cyrl=Uzbeko (cirillico)
lang.vie=Vietnamita
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Dimensione del font addPageNumbers.fontSize=Dimensione del font
addPageNumbers.fontName=Nome del font addPageNumbers.fontName=Nome del font
pdfPrompt=Scegli PDF pdfPrompt=Scegli PDF
multiPdfPrompt=Scegli 2 o più PDF multiPdfPrompt=Scegli 2 o più PDF
multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF
imgPrompt=Scegli immagine/i imgPrompt=Scegli immagine/i
genericSubmit=Invia genericSubmit=Invia
uploadLimit=Dimensione massima del file: uploadLimit=Dimensione massima del file:
uploadLimitExceededSingular=è troppo grande. La dimensione massima consentita è uploadLimitExceededSingular=è troppo grande. La dimensione massima consentita è
uploadLimitExceededPlural=sono troppo grandi. La dimensione massima consentita è uploadLimitExceededPlural=sono troppo grandi. La dimensione massima consentita è
processTimeWarning=Nota: Questo processo potrebbe richiedere fino a un minuto in base alla dimensione dei file processTimeWarning=Nota: Questo processo potrebbe richiedere fino a un minuto in base alla dimensione dei file
pageOrderPrompt=Ordine delle pagine (inserisci una lista di numeri separati da virgola): pageOrderPrompt=Ordine delle pagine (inserisci una lista di numeri separati da virgola):
pageSelectionPrompt=Selezione pagina personalizzata (inserisci un elenco separato da virgole di numeri di pagina 1,5,6 o funzioni come 2n+1) : pageSelectionPrompt=Selezione pagina personalizzata (inserisci un elenco separato da virgole di numeri di pagina 1,5,6 o funzioni come 2n+1) :
@ -37,12 +169,12 @@ sizes.small=Piccolo
sizes.medium=Medio sizes.medium=Medio
sizes.large=Grande sizes.large=Grande
sizes.x-large=Extra-Large sizes.x-large=Extra-Large
error.pdfPassword=Il documento PDF è protetto da password e la password non è stata fornita oppure non era corretta error.pdfPassword=Il documento PDF è protetto da password e la password non è stata fornita oppure non era corretta
delete=Elimina delete=Elimina
username=Nome utente username=Nome utente
password=Password password=Password
welcome=Benvenuto welcome=Benvenuto
property=Proprietà property=Proprietà
black=Nero black=Nero
white=Bianco white=Bianco
red=Rosso red=Rosso
@ -56,18 +188,18 @@ no=No
changedCredsMessage=Credenziali modificate! changedCredsMessage=Credenziali modificate!
notAuthenticatedMessage=Utente non autenticato. notAuthenticatedMessage=Utente non autenticato.
userNotFoundMessage=Utente non trovato. userNotFoundMessage=Utente non trovato.
incorrectPasswordMessage=La password attuale non è corretta. incorrectPasswordMessage=La password attuale non è corretta.
usernameExistsMessage=Il nuovo nome utente esiste già. usernameExistsMessage=Il nuovo nome utente esiste già.
invalidUsernameMessage=Nome utente non valido, il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- o deve essere un indirizzo email valido. invalidUsernameMessage=Nome utente non valido, il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- o deve essere un indirizzo email valido.
invalidPasswordMessage=La password non deve essere vuota e non deve contenere spazi all'inizio o alla fine. invalidPasswordMessage=La password non deve essere vuota e non deve contenere spazi all'inizio o alla fine.
confirmPasswordErrorMessage=La nuova password e la conferma della nuova password devono corrispondere. confirmPasswordErrorMessage=La nuova password e la conferma della nuova password devono corrispondere.
deleteCurrentUserMessage=Impossibile eliminare l'utente attualmente connesso. deleteCurrentUserMessage=Impossibile eliminare l'utente attualmente connesso.
deleteUsernameExistsMessage=Il nome utente non esiste e non può essere eliminato. deleteUsernameExistsMessage=Il nome utente non esiste e non può essere eliminato.
downgradeCurrentUserMessage=Impossibile declassare il ruolo dell'utente corrente downgradeCurrentUserMessage=Impossibile declassare il ruolo dell'utente corrente
disabledCurrentUserMessage=L'utente corrente non può essere disabilitato disabledCurrentUserMessage=L'utente corrente non può essere disabilitato
downgradeCurrentUserLongMessage=Impossibile declassare il ruolo dell'utente corrente. Pertanto, l'utente corrente non verrà visualizzato. downgradeCurrentUserLongMessage=Impossibile declassare il ruolo dell'utente corrente. Pertanto, l'utente corrente non verrà visualizzato.
userAlreadyExistsOAuthMessage=L'utente esiste già come utente OAuth2. userAlreadyExistsOAuthMessage=L'utente esiste già come utente OAuth2.
userAlreadyExistsWebMessage=L'utente esiste già come utente web. userAlreadyExistsWebMessage=L'utente esiste già come utente web.
error=Errore error=Errore
oops=Oops! oops=Oops!
help=Aiuto help=Aiuto
@ -90,7 +222,7 @@ noFileSelected=Nessun file selezionato. Caricane uno.
legal.privacy=Informativa sulla privacy legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni legal.terms=Termini e Condizioni
legal.accessibility=Accessibilità legal.accessibility=Accessibilità
legal.cookie=Informativa sui cookie legal.cookie=Informativa sui cookie
legal.impressum=Informazioni legali legal.impressum=Informazioni legali
legal.showCookieBanner=Preferenze sui cookie legal.showCookieBanner=Preferenze sui cookie
@ -98,7 +230,7 @@ legal.showCookieBanner=Preferenze sui cookie
############### ###############
# Pipeline # # Pipeline #
############### ###############
pipeline.header=Menù pipeline (Beta) pipeline.header=Menù pipeline (Beta)
pipeline.uploadButton=Caricamento personalizzato pipeline.uploadButton=Caricamento personalizzato
pipeline.configureButton=Configura pipeline.configureButton=Configura
pipeline.defaultOption=Personalizzato pipeline.defaultOption=Personalizzato
@ -124,9 +256,9 @@ pipelineOptions.validateButton=Convalidare
# ENTERPRISE EDITION # # ENTERPRISE EDITION #
######################## ########################
enterpriseEdition.button=Aggiorna alla versione Pro enterpriseEdition.button=Aggiorna alla versione Pro
enterpriseEdition.warning=Questa funzionalità è disponibile solo per gli utenti 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.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.ssoAdvert=Cerchi altre funzionalità di gestione degli utenti? Dai un'occhiata a Stirling PDF Pro
################# #################
@ -137,7 +269,7 @@ analytics.paragraph1=Stirling PDF ha opt-in analytics per aiutarci a migliorare
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.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.enable=Abilita analytics
analytics.disable=Disabilita analytics analytics.disable=Disabilita analytics
analytics.settings=È possibile modificare le impostazioni per analitycs nel file config/settings.yml analytics.settings=È possibile modificare le impostazioni per analitycs nel file config/settings.yml
############# #############
@ -145,7 +277,7 @@ analytics.settings=È possibile modificare le impostazioni per analitycs nel fil
############# #############
navbar.favorite=Preferiti navbar.favorite=Preferiti
navbar.recent=Nuovo e aggiornato di recente navbar.recent=Nuovo e aggiornato di recente
navbar.darkmode=Modalità Scura navbar.darkmode=Modalità Scura
navbar.language=Lingue navbar.language=Lingue
navbar.settings=Impostazioni navbar.settings=Impostazioni
navbar.allTools=Strumenti navbar.allTools=Strumenti
@ -164,7 +296,7 @@ navbar.sections.popular=Popolare
############# #############
settings.title=Impostazioni settings.title=Impostazioni
settings.update=Aggiornamento disponibile settings.update=Aggiornamento disponibile
settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile. settings.updateAvailable={0} è la versione attualmente installata. Una nuova versione ({1}) è disponibile.
settings.appVersion=Versione App: settings.appVersion=Versione App:
settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi): settings.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
settings.downloadOption.1=Apri in questa finestra settings.downloadOption.1=Apri in questa finestra
@ -203,7 +335,7 @@ account.signOut=Logout
account.yourApiKey=La tua API Key account.yourApiKey=La tua API Key
account.syncTitle=Sincronizza le impostazioni del browser con l'account account.syncTitle=Sincronizza le impostazioni del browser con l'account
account.settingsCompare=Confronto delle impostazioni: account.settingsCompare=Confronto delle impostazioni:
account.property=Proprietà account.property=Proprietà
account.webBrowserSettings=Impostazione del browser web account.webBrowserSettings=Impostazione del browser web
account.syncToBrowser=Sincronizza account -> Browser account.syncToBrowser=Sincronizza account -> Browser
account.syncToAccount=Sincronizza account <- Browser account.syncToAccount=Sincronizza account <- Browser
@ -217,7 +349,7 @@ adminUserSettings.addUser=Aggiungi un nuovo Utente
adminUserSettings.deleteUser=Elimina utente adminUserSettings.deleteUser=Elimina utente
adminUserSettings.confirmDeleteUser=L'utente deve essere eliminato? adminUserSettings.confirmDeleteUser=L'utente deve essere eliminato?
adminUserSettings.confirmChangeUserStatus=L'utente dovrebbe essere disabilitato/abilitato? adminUserSettings.confirmChangeUserStatus=L'utente dovrebbe essere disabilitato/abilitato?
adminUserSettings.usernameInfo=Il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- oppure deve essere un indirizzo email valido. adminUserSettings.usernameInfo=Il nome utente può contenere solo lettere, numeri e i seguenti caratteri speciali @._+- oppure deve essere un indirizzo email valido.
adminUserSettings.roles=Ruoli adminUserSettings.roles=Ruoli
adminUserSettings.role=Ruolo adminUserSettings.role=Ruolo
adminUserSettings.actions=Azioni adminUserSettings.actions=Azioni
@ -272,24 +404,24 @@ database.deleteBackupFile=Elimina file di backup
database.importBackupFile=Importa file di backup database.importBackupFile=Importa file di backup
database.createBackupFile=Crea file di backup database.createBackupFile=Crea file di backup
database.downloadBackupFile=Scarica il file di backup database.downloadBackupFile=Scarica il file di backup
database.info_1=Quando si importano i dati, è fondamentale garantire la struttura corretta. Se non sei sicuro di quello che stai facendo, chiedi consiglio e supporto a un professionista. Un errore nella struttura può causare malfunzionamenti dell'applicazione, fino alla completa impossibilità di eseguire l'applicazione. database.info_1=Quando si importano i dati, è fondamentale garantire la struttura corretta. Se non sei sicuro di quello che stai facendo, chiedi consiglio e supporto a un professionista. Un errore nella struttura può causare malfunzionamenti dell'applicazione, fino alla completa impossibilità di eseguire l'applicazione.
database.info_2=Il nome del file non ha importanza durante il caricamento. Verrà rinominato in seguito per seguire il formato backup_user__yyyyMMddHHmm.sql,garantendo una convenzione di denominazione coerente. database.info_2=Il nome del file non ha importanza durante il caricamento. Verrà rinominato in seguito per seguire il formato backup_user__yyyyMMddHHmm.sql,garantendo una convenzione di denominazione coerente.
database.submit=Importa Backup database.submit=Importa Backup
database.importIntoDatabaseSuccessed=L'importazione nel database è avvenuta con successo database.importIntoDatabaseSuccessed=L'importazione nel database è avvenuta con successo
database.backupCreated=Backup del database riuscito database.backupCreated=Backup del database riuscito
database.fileNotFound=File non trovato database.fileNotFound=File non trovato
database.fileNullOrEmpty=Il file non deve essere nullo o vuoto database.fileNullOrEmpty=Il file non deve essere nullo o vuoto
database.failedImportFile=Importazione file non riuscita database.failedImportFile=Importazione file non riuscita
database.notSupported=Questa funzione non è disponibile per la connessione al database. database.notSupported=Questa funzione non è disponibile per la connessione al database.
session.expired=La tua sessione è scaduta. Aggiorna la pagina e riprova. session.expired=La tua sessione è scaduta. Aggiorna la pagina e riprova.
session.refreshPage=Aggiorna pagina session.refreshPage=Aggiorna pagina
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############
home.desc=La tua pagina auto-gestita per modificare qualsiasi PDF. home.desc=La tua pagina auto-gestita per modificare qualsiasi PDF.
home.searchBar=Cerca funzionalità... home.searchBar=Cerca funzionalità...
home.viewPdf.title=Visualizza/Modifica PDF home.viewPdf.title=Visualizza/Modifica PDF
@ -302,7 +434,7 @@ home.showFavorites=Mostra preferiti
home.legacyHomepage=Vecchia homepage home.legacyHomepage=Vecchia homepage
home.newHomePage=Prova la nostra nuova homepage! home.newHomePage=Prova la nostra nuova homepage!
home.alphabetical=Alfabetico home.alphabetical=Alfabetico
home.globalPopularity=Popolarità home.globalPopularity=Popolarità
home.sortBy=Ordinamento: home.sortBy=Ordinamento:
home.multiTool.title=Multifunzione PDF home.multiTool.title=Multifunzione PDF
@ -310,11 +442,11 @@ home.multiTool.desc=Unisci, Ruota, Riordina, e Rimuovi pagine
multiTool.tags=Strumento multiplo,operazione multipla,interfaccia utente,trascinamento clic,front-end,lato client multiTool.tags=Strumento multiplo,operazione multipla,interfaccia utente,trascinamento clic,front-end,lato client
home.merge.title=Unisci home.merge.title=Unisci
home.merge.desc=Unisci facilmente più PDF in uno. home.merge.desc=Unisci facilmente più PDF in uno.
merge.tags=unione,operazioni sulla pagina,back-end,lato server merge.tags=unione,operazioni sulla pagina,back-end,lato server
home.split.title=Dividi home.split.title=Dividi
home.split.desc=Dividi un singolo PDF in più documenti. home.split.desc=Dividi un singolo PDF in più documenti.
split.tags=Operazioni sulla pagina,divisione,multi pagina,taglio,lato server split.tags=Operazioni sulla pagina,divisione,multi pagina,taglio,lato server
home.rotate.title=Ruota home.rotate.title=Ruota
@ -365,11 +497,11 @@ home.compressPdfs.desc=Comprimi PDF per ridurne le dimensioni.
compressPdfs.tags=comprimere,piccolo,minuscolo compressPdfs.tags=comprimere,piccolo,minuscolo
home.unlockPDFForms.title=Sblocca moduli PDF home.unlockPDFForms.title=Sblocca moduli PDF
home.unlockPDFForms.desc=Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF. home.unlockPDFForms.desc=Rimuovi la proprietà di sola lettura dei campi del modulo in un documento PDF.
unlockPDFForms.tags=rimuovi,elimina,modulo,campo,sola lettura unlockPDFForms.tags=rimuovi,elimina,modulo,campo,sola lettura
home.changeMetadata.title=Modifica Proprietà home.changeMetadata.title=Modifica Proprietà
home.changeMetadata.desc=Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF. home.changeMetadata.desc=Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.
changeMetadata.tags=Titolo,autore,data,creazione,ora,editore,produttore,statistiche changeMetadata.tags=Titolo,autore,data,creazione,ora,editore,produttore,statistiche
home.fileToPDF.title=Converti file in PDF home.fileToPDF.title=Converti file in PDF
@ -407,10 +539,10 @@ PDFToHTML.tags=contenuto web,facile da usare per il browser
home.PDFToXML.title=Da PDF a XML home.PDFToXML.title=Da PDF a XML
home.PDFToXML.desc=Converti un PDF in XML. home.PDFToXML.desc=Converti un PDF in XML.
PDFToXML.tags=estrazione dati,contenuto strutturato,interoperabilità,trasformazione,conversione PDFToXML.tags=estrazione dati,contenuto strutturato,interoperabilità,trasformazione,conversione
home.ScannerImageSplit.title=Trova/Dividi foto scansionate home.ScannerImageSplit.title=Trova/Dividi foto scansionate
home.ScannerImageSplit.desc=Estrai più foto da una singola foto o PDF. home.ScannerImageSplit.desc=Estrai più foto da una singola foto o PDF.
ScannerImageSplit.tags=separa,rileva automaticamente,scansiona,multi-foto,organizza ScannerImageSplit.tags=separa,rileva automaticamente,scansiona,multi-foto,organizza
home.sign.title=Firma home.sign.title=Firma
@ -446,7 +578,7 @@ home.removeCertSign.desc=Rimuovi la firma del certificato dal PDF
removeCertSign.tags=autenticare,PEM,P12,ufficiale,decifrare removeCertSign.tags=autenticare,PEM,P12,ufficiale,decifrare
home.pageLayout.title=Layout multipagina home.pageLayout.title=Layout multipagina
home.pageLayout.desc=Unisci più pagine di un documento PDF in un'unica pagina home.pageLayout.desc=Unisci più pagine di un documento PDF in un'unica pagina
pageLayout.tags=unire,comporre,visualizzazione singola,organizzare pageLayout.tags=unire,comporre,visualizzazione singola,organizzare
home.scalePages.title=Regola le dimensioni/scala della pagina home.scalePages.title=Regola le dimensioni/scala della pagina
@ -454,7 +586,7 @@ home.scalePages.desc=Modificare le dimensioni/scala della pagina e/o dei suoi co
scalePages.tags=ridimensionare,modificare,dimensionare,adattare scalePages.tags=ridimensionare,modificare,dimensionare,adattare
home.pipeline.title=Pipeline home.pipeline.title=Pipeline
home.pipeline.desc=Esegui più azioni sui PDF definendo script di pipeline home.pipeline.desc=Esegui più azioni sui PDF definendo script di pipeline
pipeline.tags=automatizzare,sequenziare,scriptare,elaborare in batch pipeline.tags=automatizzare,sequenziare,scriptare,elaborare in batch
home.add-page-numbers.title=Aggiungi numeri di pagina home.add-page-numbers.title=Aggiungi numeri di pagina
@ -466,7 +598,7 @@ home.auto-rename.desc=Rinomina automaticamente un file PDF in base all'intestazi
auto-rename.tags=rilevamento automatico,basato su intestazione,organizzazione,rietichettatura auto-rename.tags=rilevamento automatico,basato su intestazione,organizzazione,rietichettatura
home.adjust-contrast.title=Regola colori/contrasto home.adjust-contrast.title=Regola colori/contrasto
home.adjust-contrast.desc=Regola contrasto, saturazione e luminosità di un PDF home.adjust-contrast.desc=Regola contrasto, saturazione e luminosità di un PDF
adjust-contrast.tags=correzione del colore,messa a punto,modifica,miglioramento adjust-contrast.tags=correzione del colore,messa a punto,modifica,miglioramento
home.crop.title=Ritaglia PDF home.crop.title=Ritaglia PDF
@ -547,7 +679,7 @@ tableExtraxt.tags=CSV,Estrazione tabella,estrai,converti
home.autoSizeSplitPDF.title=Divisione automatica per dimensione/numero home.autoSizeSplitPDF.title=Divisione automatica per dimensione/numero
home.autoSizeSplitPDF.desc=Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti home.autoSizeSplitPDF.desc=Dividi un singolo PDF in più documenti in base alle dimensioni, al numero di pagine o al numero di documenti
autoSizeSplitPDF.tags=pdf,diviso,documento,organizzazione autoSizeSplitPDF.tags=pdf,diviso,documento,organizzazione
@ -556,7 +688,7 @@ home.overlay-pdfs.desc=Sovrappone i PDF sopra un altro PDF
overlay-pdfs.tags=Sovrapponi overlay-pdfs.tags=Sovrapponi
home.split-by-sections.title=Dividi PDF per sezioni home.split-by-sections.title=Dividi PDF per sezioni
home.split-by-sections.desc=Dividi ciascuna pagina di un PDF in sezioni orizzontali e verticali più piccole home.split-by-sections.desc=Dividi ciascuna pagina di un PDF in sezioni orizzontali e verticali più piccole
split-by-sections.tags=Dividi sezione,dividi,personalizza split-by-sections.tags=Dividi sezione,dividi,personalizza
home.AddStampRequest.title=Aggiungi timbro al PDF home.AddStampRequest.title=Aggiungi timbro al PDF
@ -570,7 +702,7 @@ removeImagePdf.tags=Rimuovi immagine,operazioni sulla pagina,back-end,lato serve
home.splitPdfByChapters.title=Dividi PDF per capitoli home.splitPdfByChapters.title=Dividi PDF per capitoli
home.splitPdfByChapters.desc=Dividi un PDF in più file in base alla struttura dei capitoli. home.splitPdfByChapters.desc=Dividi un PDF in più file in base alla struttura dei capitoli.
splitPdfByChapters.tags=dividi,capitoli,segnalibri,organizza splitPdfByChapters.tags=dividi,capitoli,segnalibri,organizza
home.validateSignature.title=Convalida la firma PDF home.validateSignature.title=Convalida la firma PDF
@ -581,7 +713,7 @@ validateSignature.tags=firma,verifica,convalida,pdf,certificato,firma digitale,c
replace-color.title=Sostituisci-Inverti-Colore replace-color.title=Sostituisci-Inverti-Colore
replace-color.header=Sostituisci-Inverti colore PDF replace-color.header=Sostituisci-Inverti colore PDF
home.replaceColorPdf.title=Sostituisci e inverti il colore home.replaceColorPdf.title=Sostituisci e inverti il colore
home.replaceColorPdf.desc=Sostituisci il colore del testo e dello sfondo nel PDF e inverti il colore completo del PDF per ridurre le dimensioni del file home.replaceColorPdf.desc=Sostituisci il colore del testo e dello sfondo nel PDF e inverti il ??colore completo del PDF per ridurre le dimensioni del file
replaceColorPdf.tags=Sostituisci colore, Operazioni di pagina, Back-end, lato server replaceColorPdf.tags=Sostituisci colore, Operazioni di pagina, Back-end, lato server
replace-color.selectText.1=Sostituisci o inverti le opzioni del colore replace-color.selectText.1=Sostituisci o inverti le opzioni del colore
replace-color.selectText.2=Predefinito (colori ad alto contrasto predefiniti) replace-color.selectText.2=Predefinito (colori ad alto contrasto predefiniti)
@ -609,11 +741,11 @@ login.header=Accedi
login.signin=Accedi login.signin=Accedi
login.rememberme=Ricordami login.rememberme=Ricordami
login.invalid=Nome utente o password errati. login.invalid=Nome utente o password errati.
login.locked=Il tuo account è stato bloccato. login.locked=Il tuo account è stato bloccato.
login.signinTitle=Per favore accedi login.signinTitle=Per favore accedi
login.ssoSignIn=Accedi tramite Single Sign-on login.ssoSignIn=Accedi tramite Single Sign-on
login.oAuth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA login.oAuth2AutoCreateDisabled=Creazione automatica utente OAUTH2 DISABILITATA
login.oAuth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore. login.oAuth2AdminBlockedUser=La registrazione o l'accesso degli utenti non registrati è attualmente bloccata. Si prega di contattare l'amministratore.
login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata login.oauth2RequestNotFound=Richiesta di autorizzazione non trovata
login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida login.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
login.oauth2invalidRequest=Richiesta non valida login.oauth2invalidRequest=Richiesta non valida
@ -621,8 +753,8 @@ login.oauth2AccessDenied=Accesso negato
login.oauth2InvalidTokenResponse=Risposta token non valida login.oauth2InvalidTokenResponse=Risposta token non valida
login.oauth2InvalidIdToken=Id Token non valido login.oauth2InvalidIdToken=Id Token non valido
login.relyingPartyRegistrationNotFound=Nessuna registrazione di parte affidabile trovata login.relyingPartyRegistrationNotFound=Nessuna registrazione di parte affidabile trovata
login.userIsDisabled=L'utente è disattivato, l'accesso è attualmente bloccato con questo nome utente. Si prega di contattare l'amministratore. 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.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova. login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive login.toManySessions=Hai troppe sessioni attive
login.logoutMessage=Sei stato disconnesso. login.logoutMessage=Sei stato disconnesso.
@ -692,22 +824,22 @@ getPdfInfo.header=Ottieni informazioni in PDF
getPdfInfo.submit=Ottieni informazioni getPdfInfo.submit=Ottieni informazioni
getPdfInfo.downloadJson=Scarica JSON getPdfInfo.downloadJson=Scarica JSON
getPdfInfo.summary=Riepilogo PDF getPdfInfo.summary=Riepilogo PDF
getPdfInfo.summary.encrypted=Questo PDF è crittografato, quindi potrebbe presentare problemi con alcune applicazioni getPdfInfo.summary.encrypted=Questo PDF è crittografato, quindi potrebbe presentare problemi con alcune applicazioni
getPdfInfo.summary.permissions=Questo PDF ha {0} permessi limitati che potrebbero limitare le operazioni che puoi eseguire con esso getPdfInfo.summary.permissions=Questo PDF ha {0} permessi limitati che potrebbero limitare le operazioni che puoi eseguire con esso
getPdfInfo.summary.compliance=Questo PDF è conforme allo standard {0} getPdfInfo.summary.compliance=Questo PDF è conforme allo standard {0}
getPdfInfo.summary.basicInfo=Informazioni di base getPdfInfo.summary.basicInfo=Informazioni di base
getPdfInfo.summary.docInfo=Informazioni sul documento getPdfInfo.summary.docInfo=Informazioni sul documento
getPdfInfo.summary.encrypted.alert=PDF crittografato - Questo documento è protetto da password getPdfInfo.summary.encrypted.alert=PDF crittografato - Questo documento è protetto da password
getPdfInfo.summary.not.encrypted.alert=PDF non crittografato - Nessuna protezione tramite password getPdfInfo.summary.not.encrypted.alert=PDF non crittografato - Nessuna protezione tramite password
getPdfInfo.summary.permissions.alert=Autorizzazioni limitate: {0} azioni non sono consentite getPdfInfo.summary.permissions.alert=Autorizzazioni limitate: {0} azioni non sono consentite
getPdfInfo.summary.all.permissions.alert=Tutti i permessi consentiti getPdfInfo.summary.all.permissions.alert=Tutti i permessi consentiti
getPdfInfo.summary.compliance.alert={0} Conforme getPdfInfo.summary.compliance.alert={0} Conforme
getPdfInfo.summary.no.compliance.alert=Nessuno standard di conformità getPdfInfo.summary.no.compliance.alert=Nessuno standard di conformità
getPdfInfo.summary.security.section=Stato di sicurezza getPdfInfo.summary.security.section=Stato di sicurezza
getPdfInfo.section.BasicInfo=Informazioni di base sul documento PDF, tra cui dimensione del file, numero di pagine e lingua getPdfInfo.section.BasicInfo=Informazioni di base sul documento PDF, tra cui dimensione del file, numero di pagine e lingua
getPdfInfo.section.Metadata=Metadati del documento, inclusi titolo, autore, data di creazione e altre proprietà del documento getPdfInfo.section.Metadata=Metadati del documento, inclusi titolo, autore, data di creazione e altre proprietà del documento
getPdfInfo.section.DocumentInfo=Dettagli tecnici sulla struttura e la versione del documento PDF getPdfInfo.section.DocumentInfo=Dettagli tecnici sulla struttura e la versione del documento PDF
getPdfInfo.section.Compliancy=Informazioni sulla conformità agli standard PDF(PDF/A,PDF/X,ecc.) getPdfInfo.section.Compliancy=Informazioni sulla conformità agli standard PDF(PDF/A,PDF/X,ecc.)
getPdfInfo.section.Encryption=Dettagli di sicurezza e crittografia del documento getPdfInfo.section.Encryption=Dettagli di sicurezza e crittografia del documento
getPdfInfo.section.Permissions=Impostazioni di autorizzazione del documento che controllano quali azioni possono essere eseguite getPdfInfo.section.Permissions=Impostazioni di autorizzazione del documento che controllano quali azioni possono essere eseguite
getPdfInfo.section.Other=Componenti aggiuntivi del documento come segnalibri, livelli e file incorporati getPdfInfo.section.Other=Componenti aggiuntivi del documento come segnalibri, livelli e file incorporati
@ -766,7 +898,7 @@ AddStampRequest.stampImage=Immagine del timbro
AddStampRequest.alphabet=Alfabeto AddStampRequest.alphabet=Alfabeto
AddStampRequest.fontSize=Dimensione carattere/immagine AddStampRequest.fontSize=Dimensione carattere/immagine
AddStampRequest.rotation=Rotazione AddStampRequest.rotation=Rotazione
AddStampRequest.opacity=Opacità AddStampRequest.opacity=Opacità
AddStampRequest.position=Posizione AddStampRequest.position=Posizione
AddStampRequest.overrideX=Sostituisci la coordinata X AddStampRequest.overrideX=Sostituisci la coordinata X
AddStampRequest.overrideY=Sostituisci la coordinata Y AddStampRequest.overrideY=Sostituisci la coordinata Y
@ -798,7 +930,7 @@ addPageNumbers.selectText.5=Pagine da numerare
addPageNumbers.selectText.6=Testo personalizzato addPageNumbers.selectText.6=Testo personalizzato
addPageNumbers.customTextDesc=Testo personalizzato addPageNumbers.customTextDesc=Testo personalizzato
addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc
addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n} addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n}
addPageNumbers.submit=Aggiungi numeri di pagina addPageNumbers.submit=Aggiungi numeri di pagina
@ -812,7 +944,7 @@ auto-rename.submit=Rinomina automatica
adjustContrast.title=Regola il contrasto adjustContrast.title=Regola il contrasto
adjustContrast.header=Regola il contrasto adjustContrast.header=Regola il contrasto
adjustContrast.contrast=Contrasto: adjustContrast.contrast=Contrasto:
adjustContrast.brightness=Luminosità: adjustContrast.brightness=Luminosità:
adjustContrast.saturation=Saturazione: adjustContrast.saturation=Saturazione:
adjustContrast.download=Download adjustContrast.download=Download
@ -826,13 +958,13 @@ crop.submit=Invia
#autoSplitPDF #autoSplitPDF
autoSplitPDF.title=PDF diviso automaticamente autoSplitPDF.title=PDF diviso automaticamente
autoSplitPDF.header=PDF diviso automaticamente autoSplitPDF.header=PDF diviso automaticamente
autoSplitPDF.description=Stampa, inserisci, scansiona, carica e lasciaci separare automaticamente i tuoi documenti. Non è necessario alcuno smistamento manuale. autoSplitPDF.description=Stampa, inserisci, scansiona, carica e lasciaci separare automaticamente i tuoi documenti. Non è necessario alcuno smistamento manuale.
autoSplitPDF.selectText.1=Stampa alcuni fogli divisori dal basso (il bianco e nero va bene). autoSplitPDF.selectText.1=Stampa alcuni fogli divisori dal basso (il bianco e nero va bene).
autoSplitPDF.selectText.2=Scansiona tutti i tuoi documenti contemporaneamente inserendo il foglio divisorio tra di loro. autoSplitPDF.selectText.2=Scansiona tutti i tuoi documenti contemporaneamente inserendo il foglio divisorio tra di loro.
autoSplitPDF.selectText.3=Carica il singolo file PDF scansionato di grandi dimensioni e lascia che Stirling PDF gestisca il resto. autoSplitPDF.selectText.3=Carica il singolo file PDF scansionato di grandi dimensioni e lascia che Stirling PDF gestisca il resto.
autoSplitPDF.selectText.4=Le pagine divisorie vengono rilevate e rimosse automaticamente, garantendo un documento finale ordinato. autoSplitPDF.selectText.4=Le pagine divisorie vengono rilevate e rimosse automaticamente, garantendo un documento finale ordinato.
autoSplitPDF.formPrompt=Invia PDF contenente divisori di pagina Stirling-PDF: autoSplitPDF.formPrompt=Invia PDF contenente divisori di pagina Stirling-PDF:
autoSplitPDF.duplexMode=Modalità duplex (scansione fronte e retro) autoSplitPDF.duplexMode=Modalità duplex (scansione fronte e retro)
autoSplitPDF.dividerDownload2=Scarica 'Divisore automatico (con istruzioni).pdf' autoSplitPDF.dividerDownload2=Scarica 'Divisore automatico (con istruzioni).pdf'
autoSplitPDF.submit=Invia autoSplitPDF.submit=Invia
@ -862,7 +994,7 @@ scalePages.submit=Invia
certSign.title=Firma del certificato certSign.title=Firma del certificato
certSign.header=Firma un PDF con il tuo certificato (Lavoro in corso) certSign.header=Firma un PDF con il tuo certificato (Lavoro in corso)
certSign.selectPDF=Seleziona un file PDF per la firma: certSign.selectPDF=Seleziona un file PDF per la firma:
certSign.jksNote=Nota: se il tipo di certificato non è elencato di seguito, convertilo in un file Java Keystore (.jks) utilizzando lo strumento da riga di comando keytool. Quindi, scegli l'opzione del file .jks di seguito. certSign.jksNote=Nota: se il tipo di certificato non è elencato di seguito, convertilo in un file Java Keystore (.jks) utilizzando lo strumento da riga di comando keytool. Quindi, scegli l'opzione del file .jks di seguito.
certSign.selectKey=Seleziona il file della tua chiave privata (formato PKCS#8, potrebbe essere .pem o .der): certSign.selectKey=Seleziona il file della tua chiave privata (formato PKCS#8, potrebbe essere .pem o .der):
certSign.selectCert=Seleziona il tuo file di certificato (formato X.509, potrebbe essere .pem o .der): certSign.selectCert=Seleziona il tuo file di certificato (formato X.509, potrebbe essere .pem o .der):
certSign.selectP12=Selezionare il file keystore PKCS#12 (.p12 o .pfx) (facoltativo, se fornito, dovrebbe contenere la chiave privata e il certificato): certSign.selectP12=Selezionare il file keystore PKCS#12 (.p12 o .pfx) (facoltativo, se fornito, dovrebbe contenere la chiave privata e il certificato):
@ -950,7 +1082,7 @@ flatten.submit=Appiattisci
#ScannerImageSplit #ScannerImageSplit
ScannerImageSplit.selectText.1=Soglia angolo: ScannerImageSplit.selectText.1=Soglia angolo:
ScannerImageSplit.selectText.2=Imposta il minimo angolo richiesto perché l'immagine venga ruotata (default: 10). ScannerImageSplit.selectText.2=Imposta il minimo angolo richiesto perché l'immagine venga ruotata (default: 10).
ScannerImageSplit.selectText.3=Tolleranza: ScannerImageSplit.selectText.3=Tolleranza:
ScannerImageSplit.selectText.4=Imposta lo spettro di colori attorno al colore di sfondo stimato (default: 30). ScannerImageSplit.selectText.4=Imposta lo spettro di colori attorno al colore di sfondo stimato (default: 30).
ScannerImageSplit.selectText.5=Area minima: ScannerImageSplit.selectText.5=Area minima:
@ -959,7 +1091,7 @@ ScannerImageSplit.selectText.7=Area di contorno minima:
ScannerImageSplit.selectText.8=Imposta l'area minima del contorno di una foto ScannerImageSplit.selectText.8=Imposta l'area minima del contorno di una foto
ScannerImageSplit.selectText.9=Spessore bordo: ScannerImageSplit.selectText.9=Spessore bordo:
ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1). ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1).
ScannerImageSplit.info=Python non è installato. È necessario per l'esecuzione. ScannerImageSplit.info=Python non è installato. È necessario per l'esecuzione.
#OCR #OCR
@ -972,11 +1104,11 @@ ocr.selectText.4=Pulisci il foglio in modo da evitare errori nella lettura. (non
ocr.selectText.5=Pulisci il foglio in modo da evitare errori nella lettura. (cambia il risultato) ocr.selectText.5=Pulisci il foglio in modo da evitare errori nella lettura. (cambia il risultato)
ocr.selectText.6=Ignora pagine che contengono testo interattivo, scansiona solo pagine che contengono immagini ocr.selectText.6=Ignora pagine che contengono testo interattivo, scansiona solo pagine che contengono immagini
ocr.selectText.7=Forza scansione, scansiona ogni pagina rimuovendo gli elementi originali ocr.selectText.7=Forza scansione, scansiona ogni pagina rimuovendo gli elementi originali
ocr.selectText.8=Normale (Darà errore se il PDF contiene testo) ocr.selectText.8=Normale (Darà errore se il PDF contiene testo)
ocr.selectText.9=Impostazioni extra ocr.selectText.9=Impostazioni extra
ocr.selectText.10=Modalità OCR ocr.selectText.10=Modalità OCR
ocr.selectText.11=Rimuovi immagini dopo la scansione (Rimuove TUTTE le immagini, utile solo come parte del processo di conversione) ocr.selectText.11=Rimuovi immagini dopo la scansione (Rimuove TUTTE le immagini, utile solo come parte del processo di conversione)
ocr.selectText.12=Modalità di rendering (avanzato) ocr.selectText.12=Modalità di rendering (avanzato)
ocr.help=Per favore leggi la documentazione su come usare il programma per altri linguaggi e/o uso non in Docker ocr.help=Per favore leggi la documentazione su come usare il programma per altri linguaggi e/o uso non in Docker
ocr.credit=Questo servizio utilizza Qpdf e Tesseract per l'OCR. ocr.credit=Questo servizio utilizza Qpdf e Tesseract per l'OCR.
ocr.submit=Scansiona testo nel PDF con OCR ocr.submit=Scansiona testo nel PDF con OCR
@ -1005,9 +1137,9 @@ compress.header=Comprimi PDF
compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF. compress.credit=Questo servizio utilizza qpdf per la compressione/ottimizzazione dei PDF.
compress.grayscale.label=Applica scala di grigio per la compressione compress.grayscale.label=Applica scala di grigio per la compressione
compress.selectText.1=Impostazioni di compressione compress.selectText.1=Impostazioni di compressione
compress.selectText.1.1=1-3 Compressione PDF,</br> 4-6 Compressione immagine leggera,</br> 7-9 Compressione immagine intensa Ridurrà drasticamente la qualità dell'immagine compress.selectText.1.1=1-3 Compressione PDF,</br> 4-6 Compressione immagine leggera,</br> 7-9 Compressione immagine intensa Ridurrà drasticamente la qualità dell'immagine
compress.selectText.2=Livello di ottimizzazione: compress.selectText.2=Livello di ottimizzazione:
compress.selectText.4=Modalità automatica - Regola automaticamente la qualità per ottenere le dimensioni esatte del PDF compress.selectText.4=Modalità automatica - Regola automaticamente la qualità per ottenere le dimensioni esatte del PDF
compress.selectText.5=Dimensioni PDF previste (ad es. 25 MB, 10,8 MB, 25 KB) compress.selectText.5=Dimensioni PDF previste (ad es. 25 MB, 10,8 MB, 25 KB)
compress.submit=Comprimi compress.submit=Comprimi
@ -1022,7 +1154,7 @@ addImage.submit=Aggiungi immagine
#merge #merge
merge.title=Unisci merge.title=Unisci
merge.header=Unisci 2 o più PDF merge.header=Unisci 2 o più PDF
merge.sortByName=Ordina per nome merge.sortByName=Ordina per nome
merge.sortByDate=Ordina per data merge.sortByDate=Ordina per data
merge.removeCertSign=Rimuovere la firma digitale nel file unito? merge.removeCertSign=Rimuovere la firma digitale nel file unito?
@ -1033,7 +1165,7 @@ merge.submit=Unisci
pdfOrganiser.title=Organizza pagine pdfOrganiser.title=Organizza pagine
pdfOrganiser.header=Organizza le pagine di un PDF pdfOrganiser.header=Organizza le pagine di un PDF
pdfOrganiser.submit=Riordina pagine pdfOrganiser.submit=Riordina pagine
pdfOrganiser.mode=Modalità pdfOrganiser.mode=Modalità
pdfOrganiser.mode.1=Ordine delle pagine personalizzato pdfOrganiser.mode.1=Ordine delle pagine personalizzato
pdfOrganiser.mode.2=Ordine inverso pdfOrganiser.mode.2=Ordine inverso
pdfOrganiser.mode.3=Ordinamento fronte-retro pdfOrganiser.mode.3=Ordinamento fronte-retro
@ -1074,17 +1206,17 @@ multiTool.undo=Annulla
multiTool.redo=Rifai multiTool.redo=Rifai
#decrypt #decrypt
decrypt.passwordPrompt=Questo file è protetto da password. Inserisci la password: decrypt.passwordPrompt=Questo file è protetto da password. Inserisci la password:
decrypt.cancelled=Operazione annullata per il PDF: {0} decrypt.cancelled=Operazione annullata per il PDF: {0}
decrypt.noPassword=Nessuna password fornita per il PDF crittografato: {0} decrypt.noPassword=Nessuna password fornita per il PDF crittografato: {0}
decrypt.invalidPassword=Riprova con la password corretta. decrypt.invalidPassword=Riprova con la password corretta.
decrypt.invalidPasswordHeader=Password errata o crittografia non supportata per il PDF: {0} decrypt.invalidPasswordHeader=Password errata o crittografia non supportata per il PDF: {0}
decrypt.unexpectedError=Si è verificato un errore durante l'elaborazione del file. Riprova.. decrypt.unexpectedError=Si è verificato un errore durante l'elaborazione del file. Riprova..
decrypt.serverError=Errore del server durante la decrittazione: {0} decrypt.serverError=Errore del server durante la decrittazione: {0}
decrypt.success=File decrittografato con successo. decrypt.success=File decrittografato con successo.
#multiTool-advert #multiTool-advert
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive! multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive!
#view pdf #view pdf
viewPdf.title=Visualizza/Modifica PDF viewPdf.title=Visualizza/Modifica PDF
@ -1129,7 +1261,7 @@ imageToPDF.fillPage=Riempi la pagina
imageToPDF.fitDocumentToImage=Adatta la pagina all'immagine imageToPDF.fitDocumentToImage=Adatta la pagina all'immagine
imageToPDF.maintainAspectRatio=Mantieni le proporzioni imageToPDF.maintainAspectRatio=Mantieni le proporzioni
imageToPDF.selectText.2=Ruota automaticamente PDF imageToPDF.selectText.2=Ruota automaticamente PDF
imageToPDF.selectText.3=Logica multi-file (funziona solo se ci sono più immagini) imageToPDF.selectText.3=Logica multi-file (funziona solo se ci sono più immagini)
imageToPDF.selectText.4=Unisci in un unico PDF imageToPDF.selectText.4=Unisci in un unico PDF
imageToPDF.selectText.5=Converti in PDF separati imageToPDF.selectText.5=Converti in PDF separati
@ -1140,13 +1272,13 @@ pdfToImage.header=PDF a immagine
pdfToImage.selectText=Formato immagini pdfToImage.selectText=Formato immagini
pdfToImage.singleOrMultiple=Tipo di immagine pdfToImage.singleOrMultiple=Tipo di immagine
pdfToImage.single=Unica immagine larga pdfToImage.single=Unica immagine larga
pdfToImage.multi=Più immagini pdfToImage.multi=Più immagini
pdfToImage.colorType=Tipo di colore pdfToImage.colorType=Tipo di colore
pdfToImage.color=A colori pdfToImage.color=A colori
pdfToImage.grey=Scala di grigi pdfToImage.grey=Scala di grigi
pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!) pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!)
pdfToImage.submit=Converti pdfToImage.submit=Converti
pdfToImage.info=Python non è installato.È richiesto per la conversione WebP. pdfToImage.info=Python non è installato.È richiesto per la conversione WebP.
pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1) pdfToImage.placeholder=(es. 1,2,8 o 4,7,12-16 o 2n-1)
@ -1156,11 +1288,11 @@ addPassword.header=Aggiungi password (crittografa)
addPassword.selectText.1=Seleziona PDF da crittografare addPassword.selectText.1=Seleziona PDF da crittografare
addPassword.selectText.2=Password addPassword.selectText.2=Password
addPassword.selectText.3=Lunghezza chiave addPassword.selectText.3=Lunghezza chiave
addPassword.selectText.4=Valori più grandi sono più sicuri, ma valori più piccoli offrono una compatibilità maggiore. addPassword.selectText.4=Valori più grandi sono più sicuri, ma valori più piccoli offrono una compatibilità maggiore.
addPassword.selectText.5=Permessi addPassword.selectText.5=Permessi
addPassword.selectText.6=Previeni assemblaggio del documento addPassword.selectText.6=Previeni assemblaggio del documento
addPassword.selectText.7=Previeni estrazione del contenuto addPassword.selectText.7=Previeni estrazione del contenuto
addPassword.selectText.8=Previeni estrazione per accessibilità addPassword.selectText.8=Previeni estrazione per accessibilità
addPassword.selectText.9=Previeni compilazione dei moduli addPassword.selectText.9=Previeni compilazione dei moduli
addPassword.selectText.10=Previeni modifiche addPassword.selectText.10=Previeni modifiche
addPassword.selectText.11=Previeni annotazioni addPassword.selectText.11=Previeni annotazioni
@ -1182,7 +1314,7 @@ watermark.selectText.3=Dimensione carattere:
watermark.selectText.4=Rotazione (0-360): watermark.selectText.4=Rotazione (0-360):
watermark.selectText.5=spazio orizzontale (tra ogni filigrana): watermark.selectText.5=spazio orizzontale (tra ogni filigrana):
watermark.selectText.6=spazio verticale (tra ogni filigrana): watermark.selectText.6=spazio verticale (tra ogni filigrana):
watermark.selectText.7=Opacità (0% - 100%): watermark.selectText.7=Opacità (0% - 100%):
watermark.selectText.8=Tipo di filigrana: watermark.selectText.8=Tipo di filigrana:
watermark.selectText.9=Immagine filigrana: watermark.selectText.9=Immagine filigrana:
watermark.selectText.10=Converti PDF in PDF-Immagine watermark.selectText.10=Converti PDF in PDF-Immagine
@ -1194,12 +1326,12 @@ watermark.type.2=Immagine
#Change permissions #Change permissions
permissions.title=Cambia Permessi permissions.title=Cambia Permessi
permissions.header=Cambia permessi permissions.header=Cambia permessi
permissions.warning=Attenzione: per avere questi permessi non modificabili è raccomandabile impostarli attraverso una password permissions.warning=Attenzione: per avere questi permessi non modificabili è raccomandabile impostarli attraverso una password
permissions.selectText.1=Seleziona PDF a cui cambiare permessi permissions.selectText.1=Seleziona PDF a cui cambiare permessi
permissions.selectText.2=Permessi da impostare permissions.selectText.2=Permessi da impostare
permissions.selectText.3=Previeni assemblaggio del documento permissions.selectText.3=Previeni assemblaggio del documento
permissions.selectText.4=Previeni estrazione del contenuto permissions.selectText.4=Previeni estrazione del contenuto
permissions.selectText.5=Previeni estrazione per accessibilità permissions.selectText.5=Previeni estrazione per accessibilità
permissions.selectText.6=Previeni compilazione dei moduli permissions.selectText.6=Previeni compilazione dei moduli
permissions.selectText.7=Previeni modifiche permissions.selectText.7=Previeni modifiche
permissions.selectText.8=Previeni annotazioni permissions.selectText.8=Previeni annotazioni
@ -1218,10 +1350,10 @@ removePassword.submit=Rimuovi Password
#changeMetadata #changeMetadata
changeMetadata.title=Titolo: changeMetadata.title=Titolo:
changeMetadata.header=Cambia Proprietà changeMetadata.header=Cambia Proprietà
changeMetadata.selectText.1=Imposta i dati che vuoi cambiare changeMetadata.selectText.1=Imposta i dati che vuoi cambiare
changeMetadata.selectText.2=Cancella tutte le proprietà changeMetadata.selectText.2=Cancella tutte le proprietà
changeMetadata.selectText.3=Visualizza proprietà personalizzate: changeMetadata.selectText.3=Visualizza proprietà personalizzate:
changeMetadata.author=Autore: changeMetadata.author=Autore:
changeMetadata.creationDate=Data di creazione (yyyy/MM/dd HH:mm:ss): changeMetadata.creationDate=Data di creazione (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Creatore: changeMetadata.creator=Creatore:
@ -1230,9 +1362,9 @@ changeMetadata.modDate=Data di modifica (yyyy/MM/dd HH:mm:ss):
changeMetadata.producer=Produttore: changeMetadata.producer=Produttore:
changeMetadata.subject=Oggetto: changeMetadata.subject=Oggetto:
changeMetadata.trapped=Recuperato: changeMetadata.trapped=Recuperato:
changeMetadata.selectText.4=Altre proprietà: changeMetadata.selectText.4=Altre proprietà:
changeMetadata.selectText.5=Aggiungi proprietà personalizzata: changeMetadata.selectText.5=Aggiungi proprietà personalizzata:
changeMetadata.submit=Cambia proprietà changeMetadata.submit=Cambia proprietà
#unlockPDFForms #unlockPDFForms
unlockPDFForms.title=Rimuovi la sola lettura dai campi del modulo unlockPDFForms.title=Rimuovi la sola lettura dai campi del modulo
@ -1244,9 +1376,9 @@ pdfToPDFA.title=Da PDF a PDF/A
pdfToPDFA.header=Da PDF a PDF/A pdfToPDFA.header=Da PDF a PDF/A
pdfToPDFA.credit=Questo servizio utilizza libreoffice per la conversione in PDF/A. pdfToPDFA.credit=Questo servizio utilizza libreoffice per la conversione in PDF/A.
pdfToPDFA.submit=Converti pdfToPDFA.submit=Converti
pdfToPDFA.tip=Attualmente non funziona per più input contemporaneamente pdfToPDFA.tip=Attualmente non funziona per più input contemporaneamente
pdfToPDFA.outputFormat=Formato di output pdfToPDFA.outputFormat=Formato di output
pdfToPDFA.pdfWithDigitalSignature=Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo. pdfToPDFA.pdfWithDigitalSignature=Il PDF contiene una firma digitale. Questo verrà rimosso nel passaggio successivo.
#PDFToWord #PDFToWord
@ -1308,11 +1440,11 @@ split-by-size-or-count.submit=Separa
overlay-pdfs.header=Invia file PDF in sovrapposizione overlay-pdfs.header=Invia file PDF in sovrapposizione
overlay-pdfs.baseFile.label=Seleziona File PDF di base overlay-pdfs.baseFile.label=Seleziona File PDF di base
overlay-pdfs.overlayFiles.label=Seleziona sovrapposizione file PDF overlay-pdfs.overlayFiles.label=Seleziona sovrapposizione file PDF
overlay-pdfs.mode.label=Seleziona la modalità di sovrapposizione overlay-pdfs.mode.label=Seleziona la modalità di sovrapposizione
overlay-pdfs.mode.sequential=Sovrapposizione sequenziale overlay-pdfs.mode.sequential=Sovrapposizione sequenziale
overlay-pdfs.mode.interleaved=Sovrapposizione interfogliata overlay-pdfs.mode.interleaved=Sovrapposizione interfogliata
overlay-pdfs.mode.fixedRepeat=Risolto il problema con la ripetizione della sovrapposizione overlay-pdfs.mode.fixedRepeat=Risolto il problema con la ripetizione della sovrapposizione
overlay-pdfs.counts.label=Numeri sovrapposti (per la modalità di ripetizione fissa) overlay-pdfs.counts.label=Numeri sovrapposti (per la modalità di ripetizione fissa)
overlay-pdfs.counts.placeholder=Inserisci i numeri separati da virgole (ad esempio, 2,3,1) overlay-pdfs.counts.placeholder=Inserisci i numeri separati da virgole (ad esempio, 2,3,1)
overlay-pdfs.position.label=Seleziona posizione di sovrapposizione overlay-pdfs.position.label=Seleziona posizione di sovrapposizione
overlay-pdfs.position.foreground=Primo piano overlay-pdfs.position.foreground=Primo piano
@ -1351,16 +1483,16 @@ licenses.license=Licenza
survey.nav=Sondaggio survey.nav=Sondaggio
survey.title=Sondaggio Stirling-PDF survey.title=Sondaggio Stirling-PDF
survey.description=Stirling-PDF non fa tracciamento, quindi vogliamo sentire i nostri utenti per migliorare 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.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.changes2=Con questi cambiamenti stiamo ricevendo supporto aziendale e finanziamenti retribuiti
survey.please=Ti invitiamo a prendere in considerazione la possibilità di partecipare al nostro sondaggio! 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.disabled=(Il popup del sondaggio verrà disabilitato nei prossimi aggiornamenti ma sarà disponibile a piè di pagina)
survey.button=Partecipa al sondaggio survey.button=Partecipa al sondaggio
survey.dontShowAgain=Non mostrare più survey.dontShowAgain=Non mostrare più
survey.meeting.1=Se utilizzi Stirling PDF al lavoro, saremo lieti di parlare con te. Offriamo sessioni di supporto tecnico in cambio di una sessione di individuazione dell'utente di 15 minuti. survey.meeting.1=Se utilizzi Stirling PDF al lavoro, saremo lieti di parlare con te. Offriamo sessioni di supporto tecnico in cambio di una sessione di individuazione dell'utente di 15 minuti.
survey.meeting.2=Questa è un'opportunità per: survey.meeting.2=Questa è un'opportunità per:
survey.meeting.3=Ottenere assistenza per la distribuzione, le integrazioni o la risoluzione dei problemi survey.meeting.3=Ottenere assistenza per la distribuzione, le integrazioni o la risoluzione dei problemi
survey.meeting.4=Fornire feedback diretto su prestazioni, casi limite e lacune nelle funzionalità survey.meeting.4=Fornire feedback diretto su prestazioni, casi limite e lacune nelle funzionalità
survey.meeting.5=Aiutaci a perfezionare Stirling PDF per un utilizzo aziendale nel mondo reale survey.meeting.5=Aiutaci a perfezionare Stirling PDF per un utilizzo aziendale nel mondo reale
survey.meeting.6=Se sei interessato, puoi prenotare un appuntamento direttamente con il nostro team. (Solo in inglese) survey.meeting.6=Se sei interessato, puoi prenotare un appuntamento direttamente con il nostro team. (Solo in inglese)
survey.meeting.7=Non vediamo l'ora di approfondire i tuoi casi d'uso e di migliorare ulteriormente Stirling PDF! survey.meeting.7=Non vediamo l'ora di approfondire i tuoi casi d'uso e di migliorare ulteriormente Stirling PDF!
@ -1373,7 +1505,7 @@ error.needHelp=Hai bisogno di aiuto / trovato un problema?
error.contactTip=Se i problemi persistono, non esitare a contattarci per chiedere aiuto. Puoi aprire un ticket sulla nostra pagina GitHub o contattarci tramite Discord: error.contactTip=Se i problemi persistono, non esitare a contattarci per chiedere aiuto. Puoi aprire un ticket sulla nostra pagina GitHub o contattarci tramite Discord:
error.404.head=404 - Pagina non trovata | Spiacenti, siamo inciampati nel codice! error.404.head=404 - Pagina non trovata | Spiacenti, siamo inciampati nel codice!
error.404.1=Non riusciamo a trovare la pagina che stai cercando. error.404.1=Non riusciamo a trovare la pagina che stai cercando.
error.404.2=Qualcosa è andato storto error.404.2=Qualcosa è andato storto
error.github=Apri un ticket su GitHub error.github=Apri un ticket su GitHub
error.showStack=Mostra traccia dello stack error.showStack=Mostra traccia dello stack
error.copyStack=Copia traccia dello stack error.copyStack=Copia traccia dello stack
@ -1393,10 +1525,10 @@ splitByChapters.header=Dividi PDF per capitoli
splitByChapters.bookmarkLevel=Livello segnalibro splitByChapters.bookmarkLevel=Livello segnalibro
splitByChapters.includeMetadata=Includi Metadati splitByChapters.includeMetadata=Includi Metadati
splitByChapters.allowDuplicates=Consenti duplicati splitByChapters.allowDuplicates=Consenti duplicati
splitByChapters.desc.1=Questo strumento divide un file PDF in più PDF in base alla struttura dei capitoli. 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.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.3=Includi metadati: se selezionato, i metadati del PDF originale verranno inclusi in ogni PDF diviso.
splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati. splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
splitByChapters.submit=Dividi PDF splitByChapters.submit=Dividi PDF
#File Chooser #File Chooser
@ -1429,13 +1561,13 @@ validateSignature.location=Posizione
validateSignature.noSignatures=Nessuna firma digitale trovata in questo documento validateSignature.noSignatures=Nessuna firma digitale trovata in questo documento
validateSignature.status.valid=Valida validateSignature.status.valid=Valida
validateSignature.status.invalid=Invalida validateSignature.status.invalid=Invalida
validateSignature.chain.invalid=Convalida della catena di certificati non riuscita: impossibile verificare l'identità del firmatario validateSignature.chain.invalid=Convalida della catena di certificati non riuscita: impossibile verificare l'identità del firmatario
validateSignature.trust.invalid=Certificato non presente nell'archivio attendibile: la fonte non può essere verificata validateSignature.trust.invalid=Certificato non presente nell'archivio attendibile: la fonte non può essere verificata
validateSignature.cert.expired=Il certificato è scaduto validateSignature.cert.expired=Il certificato è scaduto
validateSignature.cert.revoked=Il certificato è stato revocato validateSignature.cert.revoked=Il certificato è stato revocato
validateSignature.signature.info=Informazioni sulla firma validateSignature.signature.info=Informazioni sulla firma
validateSignature.signature=Firma validateSignature.signature=Firma
validateSignature.signature.mathValid=La firma è matematicamente valida MA: validateSignature.signature.mathValid=La firma è matematicamente valida MA:
validateSignature.selectCustomCert=File di certificato personalizzato X.509 (opzionale) validateSignature.selectCustomCert=File di certificato personalizzato X.509 (opzionale)
validateSignature.cert.info=Dettagli del certificato validateSignature.cert.info=Dettagli del certificato
validateSignature.cert.issuer=Emittente validateSignature.cert.issuer=Emittente
@ -1454,7 +1586,7 @@ validateSignature.cert.bits=bit
# Cookie banner # # Cookie banner #
#################### ####################
cookieBanner.popUp.title=Come utilizziamo i cookie cookieBanner.popUp.title=Come utilizziamo i cookie
cookieBanner.popUp.description.1=Utilizziamo cookie e altre tecnologie per migliorare l'esperienza utente di Stirling PDF, aiutandoci a perfezionare i nostri strumenti e a continuare a sviluppare funzionalità che amerai. cookieBanner.popUp.description.1=Utilizziamo cookie e altre tecnologie per migliorare l'esperienza utente di Stirling PDF, aiutandoci a perfezionare i nostri strumenti e a continuare a sviluppare funzionalità che amerai.
cookieBanner.popUp.description.2=Se preferisci non farlo, cliccando su "No grazie" verranno abilitati solo i cookie essenziali, necessari per il corretto funzionamento del sito. cookieBanner.popUp.description.2=Se preferisci non farlo, cliccando su "No grazie" verranno abilitati solo i cookie essenziali, necessari per il corretto funzionamento del sito.
cookieBanner.popUp.acceptAllBtn=Acconsento cookieBanner.popUp.acceptAllBtn=Acconsento
cookieBanner.popUp.acceptNecessaryBtn=No grazie cookieBanner.popUp.acceptNecessaryBtn=No grazie
@ -1466,12 +1598,47 @@ cookieBanner.preferencesModal.savePreferencesBtn=Salva preferenze
cookieBanner.preferencesModal.closeIconLabel=Chiusura modale cookieBanner.preferencesModal.closeIconLabel=Chiusura modale
cookieBanner.preferencesModal.serviceCounterLabel=Servizio|Servizi cookieBanner.preferencesModal.serviceCounterLabel=Servizio|Servizi
cookieBanner.preferencesModal.subtitle=Utilizzo dei cookie cookieBanner.preferencesModal.subtitle=Utilizzo dei cookie
cookieBanner.preferencesModal.description.1=Stirling PDF utilizza cookie e tecnologie simili per migliorare la tua esperienza e comprendere come vengono utilizzati i nostri strumenti. Questo ci aiuta a migliorare le prestazioni, a sviluppare le funzionalità che ti interessano e a fornire supporto continuo ai nostri utenti. cookieBanner.preferencesModal.description.1=Stirling PDF utilizza cookie e tecnologie simili per migliorare la tua esperienza e comprendere come vengono utilizzati i nostri strumenti. Questo ci aiuta a migliorare le prestazioni, a sviluppare le funzionalità che ti interessano e a fornire supporto continuo ai nostri utenti.
cookieBanner.preferencesModal.description.2=Stirling PDF non può e non potrà mai tracciare o accedere al contenuto dei documenti che utilizzi. cookieBanner.preferencesModal.description.2=Stirling PDF non può e non potrà mai tracciare o accedere al contenuto dei documenti che utilizzi.
cookieBanner.preferencesModal.description.3=La tua privacy e la tua fiducia sono al centro del nostro operato. cookieBanner.preferencesModal.description.3=La tua privacy e la tua fiducia sono al centro del nostro operato.
cookieBanner.preferencesModal.necessary.title.1=Cookie strettamente necessari cookieBanner.preferencesModal.necessary.title.1=Cookie strettamente necessari
cookieBanner.preferencesModal.necessary.title.2=Sempre abilitati cookieBanner.preferencesModal.necessary.title.2=Sempre abilitati
cookieBanner.preferencesModal.necessary.description=Questi cookie sono essenziali per il corretto funzionamento del sito web. Abilitano funzionalità fondamentali come l'impostazione delle preferenze sulla privacy, l'accesso e la compilazione di moduli, motivo per cui non possono essere disattivati. cookieBanner.preferencesModal.necessary.description=Questi cookie sono essenziali per il corretto funzionamento del sito web. Abilitano funzionalità fondamentali come l'impostazione delle preferenze sulla privacy, l'accesso e la compilazione di moduli, motivo per cui non possono essere disattivati.
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=Questi cookie ci aiutano a capire come vengono utilizzati i nostri strumenti, così possiamo concentrarci sullo sviluppo delle funzionalità che la nostra community apprezza di più. Non preoccuparti: Stirling PDF non può e non traccerà mai il contenuto dei documenti con cui lavori. cookieBanner.preferencesModal.analytics.description=Questi cookie ci aiutano a capire come vengono utilizzati i nostri strumenti, così possiamo concentrarci sullo sviluppo delle funzionalità che la nostra community apprezza di più. Non preoccuparti: Stirling PDF non può e non traccerà mai il contenuto dei documenti con cui lavori.
#fakeScan
fakeScan.title=Falsa scansione
fakeScan.header=Falsa scansione
fakeScan.description=Crea un PDF che sembra scansionato
fakeScan.selectPDF=Seleziona PDF:
fakeScan.quality=Qualità di scansione
fakeScan.quality.low=Bassa
fakeScan.quality.medium=Media
fakeScan.quality.high=Alta
fakeScan.rotation=Angolo di rotazione
fakeScan.rotation.none=Nessuno
fakeScan.rotation.slight=Lieve
fakeScan.rotation.moderate=Moderato
fakeScan.rotation.severe=Severo
fakeScan.submit=Crea una falsa scansione
#home.fakeScan
home.fakeScan.title=Falsa scansione
home.fakeScan.desc=Crea un PDF che sembra scansionato
fakeScan.tags=scansiona, simula, realistico, converti
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Abilita impostazioni di scansione avanzate
fakeScan.colorspace=Spazio colore
fakeScan.colorspace.grayscale=Scala di grigi
fakeScan.colorspace.color=Colore
fakeScan.border=Bordo (px)
fakeScan.rotate=Rotazione di base (gradi)
fakeScan.rotateVariance=Varianza di rotazione (gradi)
fakeScan.brightness=Luminosità
fakeScan.contrast=Contrasto
fakeScan.blur=Sfocatura
fakeScan.noise=Rumore
fakeScan.yellowish=Giallastro (simula carta vecchia)
fakeScan.resolution=Risoluzione (DPI)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,138 @@
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr = left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
# Language names for reuse throughout the application
lang.afr=Afrikaans
lang.amh=Amharic
lang.ara=Arabic
lang.asm=Assamese
lang.aze=Azerbaijani
lang.aze_cyrl=Azerbaijani (Cyrillic)
lang.bel=Belarusian
lang.ben=Bengali
lang.bod=Tibetan
lang.bos=Bosnian
lang.bre=Breton
lang.bul=Bulgarian
lang.cat=Catalan
lang.ceb=Cebuano
lang.ces=Czech
lang.chi_sim=Chinese (Simplified)
lang.chi_sim_vert=Chinese (Simplified, Vertical)
lang.chi_tra=Chinese (Traditional)
lang.chi_tra_vert=Chinese (Traditional, Vertical)
lang.chr=Cherokee
lang.cos=Corsican
lang.cym=Welsh
lang.dan=Danish
lang.dan_frak=Danish (Fraktur)
lang.deu=German
lang.deu_frak=German (Fraktur)
lang.div=Divehi
lang.dzo=Dzongkha
lang.ell=Greek
lang.eng=English
lang.enm=English, Middle (1100-1500)
lang.epo=Esperanto
lang.equ=Math / equation detection module
lang.est=Estonian
lang.eus=Basque
lang.fao=Faroese
lang.fas=Persian
lang.fil=Filipino
lang.fin=Finnish
lang.fra=French
lang.frk=Frankish
lang.frm=French, Middle (ca.1400-1600)
lang.fry=Western Frisian
lang.gla=Scottish Gaelic
lang.gle=Irish
lang.glg=Galician
lang.grc=Ancient Greek
lang.guj=Gujarati
lang.hat=Haitian, Haitian Creole
lang.heb=Hebrew
lang.hin=Hindi
lang.hrv=Croatian
lang.hun=Hungarian
lang.hye=Armenian
lang.iku=Inuktitut
lang.ind=Indonesian
lang.isl=Icelandic
lang.ita=Italian
lang.ita_old=Italian (Old)
lang.jav=Javanese
lang.jpn=Japanese
lang.jpn_vert=Japanese (Vertical)
lang.kan=Kannada
lang.kat=Georgian
lang.kat_old=Georgian (Old)
lang.kaz=Kazakh
lang.khm=Central Khmer
lang.kir=Kirghiz, Kyrgyz
lang.kmr=Northern Kurdish
lang.kor=Korean
lang.kor_vert=Korean (Vertical)
lang.lao=Lao
lang.lat=Latin
lang.lav=Latvian
lang.lit=Lithuanian
lang.ltz=Luxembourgish
lang.mal=Malayalam
lang.mar=Marathi
lang.mkd=Macedonian
lang.mlt=Maltese
lang.mon=Mongolian
lang.mri=Maori
lang.msa=Malay
lang.mya=Burmese
lang.nep=Nepali
lang.nld=Dutch; Flemish
lang.nor=Norwegian
lang.oci=Occitan (post 1500)
lang.ori=Oriya
lang.osd=Orientation and script detection module
lang.pan=Panjabi, Punjabi
lang.pol=Polish
lang.por=Portuguese
lang.pus=Pushto, Pashto
lang.que=Quechua
lang.ron=Romanian, Moldavian, Moldovan
lang.rus=Russian
lang.san=Sanskrit
lang.sin=Sinhala, Sinhalese
lang.slk=Slovak
lang.slk_frak=Slovak (Fraktur)
lang.slv=Slovenian
lang.snd=Sindhi
lang.spa=Spanish
lang.spa_old=Spanish (Old)
lang.sqi=Albanian
lang.srp=Serbian
lang.srp_latn=Serbian (Latin)
lang.sun=Sundanese
lang.swa=Swahili
lang.swe=Swedish
lang.syr=Syriac
lang.tam=Tamil
lang.tat=Tatar
lang.tel=Telugu
lang.tgk=Tajik
lang.tgl=Tagalog
lang.tha=Thai
lang.tir=Tigrinya
lang.ton=Tonga (Tonga Islands)
lang.tur=Turkish
lang.uig=Uighur, Uyghur
lang.ukr=Ukrainian
lang.urd=Urdu
lang.uzb=Uzbek
lang.uzb_cyrl=Uzbek (Cyrillic)
lang.vie=Vietnamese
lang.yid=Yiddish
lang.yor=Yoruba
addPageNumbers.fontSize=Lettertypegrootte addPageNumbers.fontSize=Lettertypegrootte
addPageNumbers.fontName=Lettertypenaam addPageNumbers.fontName=Lettertypenaam
pdfPrompt=Selecteer PDF('s) pdfPrompt=Selecteer PDF('s)
@ -164,7 +296,7 @@ navbar.sections.popular=Popular
############# #############
settings.title=Instellingen settings.title=Instellingen
settings.update=Update beschikbaar settings.update=Update beschikbaar
settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar. settings.updateAvailable={0} is de huidig geïnstalleerde versie. Een nieuwe versie ({1}) is beschikbaar.
settings.appVersion=App versie: settings.appVersion=App versie:
settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip): settings.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip):
settings.downloadOption.1=Open in hetzelfde venster settings.downloadOption.1=Open in hetzelfde venster
@ -310,7 +442,7 @@ home.multiTool.desc=Pagina's samenvoegen, draaien, herschikken en verwijderen
multiTool.tags=Multitool,meerdere bewerkingen,UI,klik sleep,voorkant,clientzijde,interactief,beweegbaar,verplaats multiTool.tags=Multitool,meerdere bewerkingen,UI,klik sleep,voorkant,clientzijde,interactief,beweegbaar,verplaats
home.merge.title=Samenvoegen home.merge.title=Samenvoegen
home.merge.desc=Voeg eenvoudig meerdere PDF's samen tot één. home.merge.desc=Voeg eenvoudig meerdere PDF's samen tot één.
merge.tags=samenvoegen,pagina bewerkingen,serverzijde merge.tags=samenvoegen,pagina bewerkingen,serverzijde
home.split.title=Splitsen home.split.title=Splitsen
@ -446,7 +578,7 @@ home.removeCertSign.desc=Verwijder certificaat van PDF
removeCertSign.tags=authenticeren,PEM,P12,officieel,ontsleutelen removeCertSign.tags=authenticeren,PEM,P12,officieel,ontsleutelen
home.pageLayout.title=Multi-pagina indeling home.pageLayout.title=Multi-pagina indeling
home.pageLayout.desc=Voeg meerdere pagina's van een PDF-document samen op één pagina home.pageLayout.desc=Voeg meerdere pagina's van een PDF-document samen op één pagina
pageLayout.tags=samenvoegen,composiet,enkel-zicht,organiseren pageLayout.tags=samenvoegen,composiet,enkel-zicht,organiseren
home.scalePages.title=Aanpassen paginaformaat/schaal home.scalePages.title=Aanpassen paginaformaat/schaal
@ -454,7 +586,7 @@ home.scalePages.desc=Wijzig de grootte/schaal van een pagina en/of de inhoud erv
scalePages.tags=resize,aanpassen,dimensie,aanpassen scalePages.tags=resize,aanpassen,dimensie,aanpassen
home.pipeline.title=Pijplijn home.pipeline.title=Pijplijn
home.pipeline.desc=Voer meerdere acties uit op PDF's door pipelinescripts te definiëren home.pipeline.desc=Voer meerdere acties uit op PDF's door pipelinescripts te definiëren
pipeline.tags=automatiseren,volgorde,gescrript,batch-verwerking pipeline.tags=automatiseren,volgorde,gescrript,batch-verwerking
home.add-page-numbers.title=Paginanummers toevoegen home.add-page-numbers.title=Paginanummers toevoegen
@ -524,13 +656,13 @@ home.extractPage.desc=Extraheert geselecteerde pagina's uit PDF
extractPage.tags=extraheren extractPage.tags=extraheren
home.PdfToSinglePage.title=PDF naar één grote pagina home.PdfToSinglePage.title=PDF naar één grote pagina
home.PdfToSinglePage.desc=Voegt alle PDF-pagina's samen tot één grote pagina home.PdfToSinglePage.desc=Voegt alle PDF-pagina's samen tot één grote pagina
PdfToSinglePage.tags=één pagina PdfToSinglePage.tags=één pagina
home.showJS.title=Toon Javascript home.showJS.title=Toon Javascript
home.showJS.desc=Zoekt en toont ieder script dat in een PDF is geïnjecteerd home.showJS.desc=Zoekt en toont ieder script dat in een PDF is geïnjecteerd
showJS.tags=JS showJS.tags=JS
home.autoRedact.title=Automatisch censureren home.autoRedact.title=Automatisch censureren
@ -768,8 +900,8 @@ AddStampRequest.fontSize=Tekst/afbeelding grootte
AddStampRequest.rotation=Rotatie AddStampRequest.rotation=Rotatie
AddStampRequest.opacity=Transparantie AddStampRequest.opacity=Transparantie
AddStampRequest.position=Positie AddStampRequest.position=Positie
AddStampRequest.overrideX=X coördinaat overschrijven AddStampRequest.overrideX=X coördinaat overschrijven
AddStampRequest.overrideY=Y coördinaat overschrijven AddStampRequest.overrideY=Y coördinaat overschrijven
AddStampRequest.customMargin=Aangepaste marge AddStampRequest.customMargin=Aangepaste marge
AddStampRequest.customColor=Aangepaste tekstkleur AddStampRequest.customColor=Aangepaste tekstkleur
AddStampRequest.submit=Indienen AddStampRequest.submit=Indienen
@ -863,12 +995,12 @@ certSign.title=Certificaat ondertekening
certSign.header=Onderteken een PDF met je certificaat (in ontwikkeling) certSign.header=Onderteken een PDF met je certificaat (in ontwikkeling)
certSign.selectPDF=Selecteer een PDF-bestand voor ondertekening: certSign.selectPDF=Selecteer een PDF-bestand voor ondertekening:
certSign.jksNote=Let op: als het certificaattype hieronder niet staat, converteer het dan naar een Java Keystore (.jks) bestand met de keytool command line tool. Kies vervolgens de .jks bestandsoptie. certSign.jksNote=Let op: als het certificaattype hieronder niet staat, converteer het dan naar een Java Keystore (.jks) bestand met de keytool command line tool. Kies vervolgens de .jks bestandsoptie.
certSign.selectKey=Selecteer je privésleutelbestand (PKCS#8 formaat, kan .pem of .der zijn): certSign.selectKey=Selecteer je privésleutelbestand (PKCS#8 formaat, kan .pem of .der zijn):
certSign.selectCert=Selecteer je certificaatbestand (X.509 formaat, kan .pem of .der zijn): certSign.selectCert=Selecteer je certificaatbestand (X.509 formaat, kan .pem of .der zijn):
certSign.selectP12=Selecteer je PKCS#12 Sleutelopslagbestand (.p12 of .pfx) (Optioneel, indien verstrekt, moet het je privésleutel en certificaat bevatten): certSign.selectP12=Selecteer je PKCS#12 Sleutelopslagbestand (.p12 of .pfx) (Optioneel, indien verstrekt, moet het je privésleutel en certificaat bevatten):
certSign.selectJKS=Selecteer je Java Keystore bestand (.jks of .keystore): certSign.selectJKS=Selecteer je Java Keystore bestand (.jks of .keystore):
certSign.certType=Certificaattype certSign.certType=Certificaattype
certSign.password=Voer je sleutelopslag of privésleutel wachtwoord in (indien van toepassing): certSign.password=Voer je sleutelopslag of privésleutel wachtwoord in (indien van toepassing):
certSign.showSig=Toon handtekening certSign.showSig=Toon handtekening
certSign.reason=Reden certSign.reason=Reden
certSign.location=Locatie certSign.location=Locatie
@ -908,8 +1040,8 @@ compare.highlightColor.2=Hervormingskleur 2:
compare.document.1=Document 1 compare.document.1=Document 1
compare.document.2=Document 2 compare.document.2=Document 2
compare.submit=Vergelijken compare.submit=Vergelijken
compare.complex.message=Eén of beide van de bijgewerkte documenten zijn grote bestanden, het vergelijken kan mogelijk minder nauwkeurig zijn. compare.complex.message=Eén of beide van de bijgewerkte documenten zijn grote bestanden, het vergelijken kan mogelijk minder nauwkeurig zijn.
compare.large.file.message=Eén of beiden van de bijgewerkte documenten zijn te groot om verwerkt te worden. compare.large.file.message=Eén of beiden van de bijgewerkte documenten zijn te groot om verwerkt te worden.
compare.no.text.message=Een of beide geselecteerde PDF-bestanden bevatten geen tekstinhoud. Kies a.u.b. PDF-bestanden met tekst voor vergelijking. compare.no.text.message=Een of beide geselecteerde PDF-bestanden bevatten geen tekstinhoud. Kies a.u.b. PDF-bestanden met tekst voor vergelijking.
#sign #sign
@ -959,7 +1091,7 @@ ScannerImageSplit.selectText.7=Minimum contour oppervlakte:
ScannerImageSplit.selectText.8=Stelt de minimale contour oppervlakte drempel in voor een foto ScannerImageSplit.selectText.8=Stelt de minimale contour oppervlakte drempel in voor een foto
ScannerImageSplit.selectText.9=Randgrootte: ScannerImageSplit.selectText.9=Randgrootte:
ScannerImageSplit.selectText.10=Stelt de grootte van de toegevoegde en verwijderde rand in om witte randen in de uitvoer te voorkomen (standaard: 1). ScannerImageSplit.selectText.10=Stelt de grootte van de toegevoegde en verwijderde rand in om witte randen in de uitvoer te voorkomen (standaard: 1).
ScannerImageSplit.info=Python is niet geïnstalleerd. Het wordt vereist om te worden uitgevoerd. ScannerImageSplit.info=Python is niet geïnstalleerd. Het wordt vereist om te worden uitgevoerd.
#OCR #OCR
@ -985,7 +1117,7 @@ ocr.submit=Verwerk PDF met OCR
#extractImages #extractImages
extractImages.title=Afbeeldingen extraheren extractImages.title=Afbeeldingen extraheren
extractImages.header=Afbeeldingen extraheren extractImages.header=Afbeeldingen extraheren
extractImages.selectText=Selecteer het beeldformaat voor geëxtraheerde afbeeldingen extractImages.selectText=Selecteer het beeldformaat voor geëxtraheerde afbeeldingen
extractImages.allowDuplicates=Dubbele afbeeldingen opslaan extractImages.allowDuplicates=Dubbele afbeeldingen opslaan
extractImages.submit=Extraheer extractImages.submit=Extraheer
@ -1130,7 +1262,7 @@ imageToPDF.fitDocumentToImage=Pagina passend maken voor afbeelding
imageToPDF.maintainAspectRatio=Beeldverhoudingen behouden imageToPDF.maintainAspectRatio=Beeldverhoudingen behouden
imageToPDF.selectText.2=PDF automatisch draaien imageToPDF.selectText.2=PDF automatisch draaien
imageToPDF.selectText.3=Meervoudige bestandslogica (Alleen ingeschakeld bij werken met meerdere afbeeldingen) imageToPDF.selectText.3=Meervoudige bestandslogica (Alleen ingeschakeld bij werken met meerdere afbeeldingen)
imageToPDF.selectText.4=Voeg samen in één PDF imageToPDF.selectText.4=Voeg samen in één PDF
imageToPDF.selectText.5=Zet om naar afzonderlijke PDF's imageToPDF.selectText.5=Zet om naar afzonderlijke PDF's
@ -1139,14 +1271,14 @@ pdfToImage.title=PDF naar afbeelding
pdfToImage.header=PDF naar afbeelding pdfToImage.header=PDF naar afbeelding
pdfToImage.selectText=Afbeeldingsformaat pdfToImage.selectText=Afbeeldingsformaat
pdfToImage.singleOrMultiple=Resultaattype van pagina naar afbeelding pdfToImage.singleOrMultiple=Resultaattype van pagina naar afbeelding
pdfToImage.single=Eén grote afbeelding die alle pagina's combineert pdfToImage.single=Eén grote afbeelding die alle pagina's combineert
pdfToImage.multi=Meerdere afbeeldingen, één afbeelding per pagina pdfToImage.multi=Meerdere afbeeldingen, één afbeelding per pagina
pdfToImage.colorType=Kleurtype pdfToImage.colorType=Kleurtype
pdfToImage.color=Kleur pdfToImage.color=Kleur
pdfToImage.grey=Grijstinten pdfToImage.grey=Grijstinten
pdfToImage.blackwhite=Zwart en wit (kan data verliezen!) pdfToImage.blackwhite=Zwart en wit (kan data verliezen!)
pdfToImage.submit=Omzetten pdfToImage.submit=Omzetten
pdfToImage.info=Python is niet geïnstalleerd. Vereist voor WebP-conversie. pdfToImage.info=Python is niet geïnstalleerd. Vereist voor WebP-conversie.
pdfToImage.placeholder=(bijv. 1,2,8 of 4,7,12-16 of 2n-1) pdfToImage.placeholder=(bijv. 1,2,8 of 4,7,12-16 of 2n-1)
@ -1310,7 +1442,7 @@ overlay-pdfs.baseFile.label=Selecteer basis PDF-bestand
overlay-pdfs.overlayFiles.label=Selecteer overlappende PDF-bestanden overlay-pdfs.overlayFiles.label=Selecteer overlappende PDF-bestanden
overlay-pdfs.mode.label=Selecteer overlappingsmodus overlay-pdfs.mode.label=Selecteer overlappingsmodus
overlay-pdfs.mode.sequential=Sequentieel overlappen overlay-pdfs.mode.sequential=Sequentieel overlappen
overlay-pdfs.mode.interleaved=Geïnterlinieerd overlappen overlay-pdfs.mode.interleaved=Geïnterlinieerd overlappen
overlay-pdfs.mode.fixedRepeat=Overlappen met vaste herhaling overlay-pdfs.mode.fixedRepeat=Overlappen met vaste herhaling
overlay-pdfs.counts.label=Aantal keren overlappen (voor vaste herhalings modus) overlay-pdfs.counts.label=Aantal keren overlappen (voor vaste herhalings modus)
overlay-pdfs.counts.placeholder=Voer door komma's gescheiden aantallen in (bijv., 2,3,1) overlay-pdfs.counts.placeholder=Voer door komma's gescheiden aantallen in (bijv., 2,3,1)
@ -1328,7 +1460,7 @@ split-by-sections.vertical.label=Verticale secties
split-by-sections.horizontal.placeholder=Voer het aantal horizontale secties in split-by-sections.horizontal.placeholder=Voer het aantal horizontale secties in
split-by-sections.vertical.placeholder=Voer het aantal verticale secties in split-by-sections.vertical.placeholder=Voer het aantal verticale secties in
split-by-sections.submit=PDF splitsen split-by-sections.submit=PDF splitsen
split-by-sections.merge=Samenvoegen in één PDF split-by-sections.merge=Samenvoegen in één PDF
#printFile #printFile
@ -1348,14 +1480,14 @@ licenses.version=Versie
licenses.license=Licentie licenses.license=Licentie
#survey #survey
survey.nav=Enquête survey.nav=Enquête
survey.title=Stirling-PDF Enquête survey.title=Stirling-PDF Enquête
survey.description=Stirling-PDF heeft geen tracking, dus we willen van onze gebruikers horen om Stirling-PDF te verbeteren. survey.description=Stirling-PDF heeft geen tracking, dus we willen van onze gebruikers horen om Stirling-PDF te verbeteren.
survey.changes=Stirling-PDF is sinds de laatste enquête veranderd! Zie hier onze blogpost voor meer informatie: survey.changes=Stirling-PDF is sinds de laatste enquête veranderd! Zie hier onze blogpost voor meer informatie:
survey.changes2=Met deze veranderingen krijgen we betaalde bedrijfsondersteuning en financiering survey.changes2=Met deze veranderingen krijgen we betaalde bedrijfsondersteuning en financiering
survey.please=Overweeg alstublieft om onze enquête in te vullen! survey.please=Overweeg alstublieft om onze enquête in te vullen!
survey.disabled=(Enquête popup wordt in een toekomstige update weggehaald, maar is beschikbaar aan de onderkant van de pagina.) survey.disabled=(Enquête popup wordt in een toekomstige update weggehaald, maar is beschikbaar aan de onderkant van de pagina.)
survey.button=Vul enquête in. survey.button=Vul enquête in.
survey.dontShowAgain=Niet weer tonen survey.dontShowAgain=Niet weer tonen
survey.meeting.1=If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session. survey.meeting.1=If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session.
survey.meeting.2=This is a chance to: survey.meeting.2=This is a chance to:
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner # # Cookie banner #
#################### ####################
cookieBanner.popUp.title=How we use Cookies cookieBanner.popUp.title=How we use Cookies
cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for youhelping us improve our tools and keep building features you'll love. cookieBanner.popUp.description.1=We use cookies and other technologies to make Stirling PDF work better for you?helping us improve our tools and keep building features you'll love.
cookieBanner.popUp.description.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly. cookieBanner.popUp.description.2=If you?d rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
cookieBanner.popUp.acceptAllBtn=Okay cookieBanner.popUp.acceptAllBtn=Okay
cookieBanner.popUp.acceptNecessaryBtn=No Thanks cookieBanner.popUp.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
cookieBanner.preferencesModal.subtitle=Cookie Usage cookieBanner.preferencesModal.subtitle=Cookie Usage
cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users. cookieBanner.preferencesModal.description.1=Stirling PDF uses cookies and similar technologies to enhance your experience and understand how our tools are used. This helps us improve performance, develop the features you care about, and provide ongoing support to our users.
cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never—track or access the content of the documents you use. cookieBanner.preferencesModal.description.2=Stirling PDF cannot?and will never?track or access the content of the documents you use.
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do. cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
cookieBanner.preferencesModal.necessary.title.2=Always Enabled cookieBanner.preferencesModal.necessary.title.2=Always Enabled
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they cant be turned off. cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms?which is why they can?t be turned off.
cookieBanner.preferencesModal.analytics.title=Analytics cookieBanner.preferencesModal.analytics.title=Analytics
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assuredStirling PDF cannot and will never track the content of the documents you work with. cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
#fakeScan
fakeScan.title=Fake Scan
fakeScan.header=Fake Scan
fakeScan.description=Create a PDF that looks like it was scanned
fakeScan.selectPDF=Select PDF:
fakeScan.quality=Scan Quality
fakeScan.quality.low=Low
fakeScan.quality.medium=Medium
fakeScan.quality.high=High
fakeScan.rotation=Rotation Angle
fakeScan.rotation.none=None
fakeScan.rotation.slight=Slight
fakeScan.rotation.moderate=Moderate
fakeScan.rotation.severe=Severe
fakeScan.submit=Create Fake Scan
#home.fakeScan
home.fakeScan.title=Fake Scan
home.fakeScan.desc=Create a PDF that looks like it was scanned
fakeScan.tags=scan,simulate,realistic,convert
# FakeScan advanced settings (frontend)
fakeScan.advancedSettings=Enable Advanced Scan Settings
fakeScan.colorspace=Colorspace
fakeScan.colorspace.grayscale=Grayscale
fakeScan.colorspace.color=Color
fakeScan.border=Border (px)
fakeScan.rotate=Base Rotation (degrees)
fakeScan.rotateVariance=Rotation Variance (degrees)
fakeScan.brightness=Brightness
fakeScan.contrast=Contrast
fakeScan.blur=Blur
fakeScan.noise=Noise
fakeScan.yellowish=Yellowish (simulate old paper)
fakeScan.resolution=Resolution (DPI)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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