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:
DISABLE_ADDITIONAL_FEATURES: false
- name: Upload Test Reports
- name: Check Test Reports Exist
id: check-reports
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
with:
name: test-reports-jdk-${{ matrix.jdk-version }}
path: |
build/reports/tests/
build/test-results/
build/reports/problems/
/common/build/reports/tests/
/common/build/test-results/
/common/build/reports/problems/
stirling-pdf/build/reports/tests/
stirling-pdf/build/test-results/
stirling-pdf/build/reports/problems/
common/build/reports/tests/
common/build/test-results/
common/build/reports/problems/
proprietary/build/reports/tests/
proprietary/build/test-results/
proprietary/build/reports/problems/
retention-days: 3
check-licence:

View File

@ -5,8 +5,7 @@ FROM alpine:3.22.0@sha256:8a1f59ffb675680d47db6337b49d22281a139e9d709335b492be02
COPY scripts /scripts
COPY pipeline /pipeline
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 build/libs/*.jar app.jar
COPY stirling-pdf/build/libs/*.jar app.jar
ARG VERSION_TAG

View File

@ -5,6 +5,7 @@ COPY build.gradle .
COPY settings.gradle .
COPY gradlew .
COPY gradle gradle/
COPY stirling-pdf/build.gradle stirling-pdf/.
COPY common/build.gradle common/.
COPY proprietary/build.gradle proprietary/.
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 pipeline /pipeline
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

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/installFonts.sh /scripts/installFonts.sh
COPY pipeline /pipeline
COPY build/libs/*.jar app.jar
COPY stirling-pdf/build/libs/*.jar app.jar
# Set up necessary directories and permissions
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \

View File

@ -1,8 +1,8 @@
plugins {
id "java"
id "jacoco"
id "org.springframework.boot" version "3.5.0"
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 "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.6"
@ -50,7 +50,6 @@ sourceSets {
&& 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/**'
}
@ -75,7 +74,7 @@ sourceSets {
allprojects {
group = 'stirling.software'
version = '0.46.2'
version = '1.0.0'
configurations.configureEach {
exclude group: 'commons-logging', module: 'commons-logging'
@ -130,6 +129,7 @@ subprojects {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
}
tasks.withType(JavaCompile).configureEach {
@ -146,6 +146,11 @@ subprojects {
}
}
tasks.withType(JavaCompile).configureEach {
options.encoding = "UTF-8"
dependsOn "spotlessApply"
}
licenseReport {
renderers = [new JsonReportRenderer()]
allowedLicensesFile = new File("$projectDir/allowed-licenses.json")
@ -468,6 +473,7 @@ spotless {
target sourceSets.main.allJava
target project(':common').sourceSets.main.allJava
target project(':proprietary').sourceSets.main.allJava
target project(':stirling-pdf').sourceSets.main.allJava
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
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.12.2'
}
tasks.named("test") {
useJUnitPlatform()
}
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
doLast {
@ -529,6 +540,7 @@ tasks.register('writeVersion') {
}
processResources.dependsOn(writeVersion)
project(':stirling-pdf').tasks.bootJar.dependsOn(writeVersion)
tasks.register('printVersion') {
doLast {
@ -545,3 +557,22 @@ tasks.register('printMacVersion') {
tasks.named('generateOpenApiDocs') {
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 {
api 'org.springframework.boot:spring-boot-starter-web'
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
@ -12,4 +17,4 @@ dependencies {
api 'org.snakeyaml:snakeyaml-engine:2.9'
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.8"
api 'jakarta.mail:jakarta.mail-api:2.1.3'
}
}

View File

@ -1,7 +1,5 @@
package stirling.software.common.configuration;
import io.github.pixee.security.SystemCommand;
import jakarta.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
@ -10,25 +8,22 @@ import java.util.List;
import java.util.Locale;
import java.util.Properties;
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.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.Scope;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.model.ApplicationProperties;
@Lazy
@ -253,9 +248,35 @@ public class AppConfig {
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")
public boolean disablePixel() {
return Boolean.getBoolean(env.getProperty("DISABLE_PIXEL"));
return Boolean.parseBoolean(env.getProperty("DISABLE_PIXEL", "false"));
}
@Bean(name = "machineType")

View File

@ -1,7 +1,9 @@
repositories {
maven { url = "https://build.shibboleth.net/maven/releases" }
}
bootRun {
enabled = false
}
dependencies {
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;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
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.enumeration.Role;
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.TeamService;
import stirling.software.proprietary.security.service.UserService;
@Slf4j
@ -22,9 +26,8 @@ import stirling.software.proprietary.security.service.UserService;
public class InitialSecuritySetup {
private final UserService userService;
private final TeamService teamService;
private final ApplicationProperties applicationProperties;
private final DatabaseServiceInterface databaseService;
@PostConstruct
@ -40,6 +43,7 @@ public class InitialSecuritySetup {
}
userService.migrateOauth2ToSSO();
assignUsersToDefaultTeamIfMissing();
initializeInternalApiUser();
} catch (IllegalArgumentException | SQLException | UnsupportedProviderException 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 {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
@ -58,7 +75,9 @@ public class InitialSecuritySetup {
&& !initialPassword.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);
} else {
createDefaultAdminUser();
@ -70,7 +89,9 @@ public class InitialSecuritySetup {
String defaultPassword = "stirling";
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);
}
}
@ -78,10 +99,13 @@ public class InitialSecuritySetup {
private void initializeInternalApiUser()
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
Team team = teamService.getOrCreateInternalTeam();
userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId());
team,
Role.INTERNAL_API_USER.getRoleId(),
false);
userService.addApiKeyToUser(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;
@ -38,11 +38,14 @@ import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.oauth2.GitHubProvider;
import stirling.software.common.model.oauth2.GoogleProvider;
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.model.Authority;
import stirling.software.proprietary.security.model.SessionEntity;
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.service.TeamService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@Controller
@ -57,16 +60,19 @@ public class AccountWebController {
// Assuming you have a repository for user operations
private final UserRepository userRepository;
private final boolean runningEE;
private final TeamRepository teamRepository;
public AccountWebController(
ApplicationProperties applicationProperties,
SessionPersistentRegistry sessionPersistentRegistry,
UserRepository userRepository,
TeamRepository teamRepository,
@Qualifier("runningEE") boolean runningEE) {
this.applicationProperties = applicationProperties;
this.sessionPersistentRegistry = sessionPersistentRegistry;
this.userRepository = userRepository;
this.runningEE = runningEE;
this.teamRepository = teamRepository;
}
@GetMapping("/login")
@ -210,7 +216,7 @@ public class AccountWebController {
@GetMapping("/adminSettings")
public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll();
List<User> allUsers = userRepository.findAllWithTeam();
Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status
@ -221,14 +227,27 @@ public class AccountWebController {
while (iterator.hasNext()) {
User user = iterator.next();
if (user != null) {
boolean shouldRemove = false;
// Check if user is an INTERNAL_API_USER
for (Authority authority : user.getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove();
shouldRemove = true;
roleDetails.remove(Role.INTERNAL_API_USER.getRoleId());
// Break out of the inner loop once the user is removed
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
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
boolean hasActiveSession = false;
@ -331,6 +350,13 @@ public class AccountWebController {
model.addAttribute("activeUsers", activeUsers);
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());
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import lombok.Getter;
@ -21,8 +22,12 @@ import stirling.software.common.model.exception.UnsupportedProviderException;
@Slf4j
@Getter
@Configuration
@EnableJpaRepositories(basePackages = "stirling.software.proprietary.security.database.repository")
@EntityScan({"stirling.software.proprietary.security.model"})
@EnableJpaRepositories(
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 final String DATASOURCE_DEFAULT_URL;
@ -55,6 +60,7 @@ public class DatabaseConfig {
*/
@Bean
@Qualifier("dataSource")
@Primary
public DataSource dataSource() throws UnsupportedProviderException {
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 org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
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.annotation.Order;
@ -28,18 +29,23 @@ public class EEAppConfig {
migrateEnterpriseSettingsToPremium(this.applicationProperties);
}
@Profile("security")
@Bean(name = "runningProOrHigher")
@Qualifier("runningProOrHigher")
@Primary
public boolean runningProOrHigher() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
}
@Profile("security")
@Bean(name = "license")
@Primary
public String licenseType() {
return licenseKeyChecker.getPremiumLicenseEnabledResult().name();
}
@Profile("security")
@Bean(name = "runningEE")
@Primary
public boolean runningEnterprise() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE;
}
@ -49,7 +55,9 @@ public class EEAppConfig {
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
}
@Profile("security")
@Bean(name = "GoogleDriveEnabled")
@Primary
public boolean googleDriveEnabled() {
return runningProOrHigher()
&& 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.HttpServletResponse;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
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.enumeration.Role;
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.User;
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.service.TeamService;
import stirling.software.proprietary.security.service.UserService;
import stirling.software.proprietary.security.session.SessionPersistentRegistry;
@ -50,7 +55,8 @@ public class UserController {
private final UserService userService;
private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
private final TeamRepository teamRepository;
private final UserRepository userRepository;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
@ -60,7 +66,13 @@ public class UserController {
return "register";
}
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) {
return "redirect:/login?messageType=invalidUsername";
}
@ -200,6 +212,7 @@ public class UserController {
@RequestParam(name = "username", required = true) String username,
@RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
@RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange)
@ -233,13 +246,29 @@ public class UserController {
// If the role ID is not valid, redirect with an error message
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())) {
userService.saveUser(username, AuthenticationType.SSO, role);
userService.saveUser(username, AuthenticationType.SSO, effectiveTeamId, role);
} else {
if (password.isBlank()) {
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange);
userService.saveUser(username, password, effectiveTeamId, role, forceChange);
}
return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user
@ -248,9 +277,11 @@ public class UserController {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@PostMapping("/admin/changeRole")
@Transactional
public RedirectView changeRole(
@RequestParam(name = "username") String username,
@RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
Authentication authentication)
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@ -278,6 +309,26 @@ public class UserController {
return new RedirectView("/adminSettings?messageType=invalidRole", true);
}
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);
return new RedirectView(
"/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("lastRequest") Date lastRequest,
@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.stereotype.Repository;
import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.model.User;
@Repository
@ -22,4 +23,17 @@ public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByApiKey(String apiKey);
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 stirling.software.common.model.enumeration.Role;
import stirling.software.proprietary.model.Team;
@Entity
@Table(name = "users")
@ -57,6 +58,10 @@ public class User implements Serializable {
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user")
private Set<Authority> authorities = new HashSet<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
@ElementCollection
@MapKeyColumn(name = "setting_key")
@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.Optional;
import java.util.UUID;
import java.util.function.Supplier;
import org.springframework.context.MessageSource;
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.exception.UnsupportedProviderException;
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.UserRepository;
import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.Authority;
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.session.SessionPersistentRegistry;
@ -45,7 +48,7 @@ import stirling.software.proprietary.security.session.SessionPersistentRegistry;
public class UserService implements UserServiceInterface {
private final UserRepository userRepository;
private final TeamRepository teamRepository;
private final AuthorityRepository authorityRepository;
private final PasswordEncoder passwordEncoder;
@ -162,7 +165,7 @@ public class UserService implements UserServiceInterface {
public void saveUser(String username, AuthenticationType authenticationType)
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) {
@ -173,71 +176,98 @@ public class UserService implements UserServiceInterface {
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 {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
user.setEnabled(true);
user.setFirstLogin(false);
user.addAuthority(new Authority(role, user));
user.setAuthenticationType(authenticationType);
userRepository.save(user);
databaseService.exportDatabase();
return saveUserCore(
username, // username
null, // password
authenticationType, // authenticationType
teamId, // teamId
null, // team
role, // role
false, // firstLogin
true // enabled
);
}
public void saveUser(String username, String password)
public User saveUser(
String username, AuthenticationType authenticationType, Team team, String role)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB);
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
userRepository.save(user);
databaseService.exportDatabase();
return saveUserCore(
username, // username
null, // password
authenticationType, // authenticationType
null, // teamId
team, // team
role, // role
false, // firstLogin
true // enabled
);
}
public void saveUser(String username, String password, String role, boolean firstLogin)
public User saveUser(String username, String password, Long teamId)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin);
userRepository.save(user);
databaseService.exportDatabase();
return saveUserCore(
username, // username
password, // password
AuthenticationType.WEB, // authenticationType
teamId, // teamId
null, // team
Role.USER.getRoleId(), // role
false, // firstLogin
true // enabled
);
}
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 {
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 {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
user.setEnabled(enabled);
user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin);
userRepository.save(user);
databaseService.exportDatabase();
return saveUserCore(
username, // username
password, // password
AuthenticationType.WEB, // authenticationType
teamId, // teamId
null, // team
role, // role
firstLogin, // firstLogin
true // enabled
);
}
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) {
@ -345,6 +375,111 @@ public class UserService implements UserServiceInterface {
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) {
// Checks whether the simple username is formatted correctly
// Regular expression for user name: Min. 3 characters, max. 50 characters
@ -464,7 +599,6 @@ public class UserService implements UserServiceInterface {
}
}
@Override
public long getTotalUsersCount() {
// Count all users in the database
long userCount = userRepository.count();
@ -474,4 +608,12 @@ public class UserService implements UserServiceInterface {
}
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
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
}
rootProject.name = 'Stirling-PDF'
rootProject.name = 'Stirling PDF'
include 'stirling-pdf', 'common', 'proprietary'

View File

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

View File

@ -1,8 +1,17 @@
apply plugin: 'org.springframework.boot'
repositories {
maven { url = 'https://build.shibboleth.net/maven/releases' }
maven { url = 'https://maven.pkg.github.com/jcefmaven/jcefmaven' }
}
configurations {
developmentOnly
runtimeClasspath {
extendsFrom developmentOnly
}
}
dependencies {
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
@ -71,25 +80,58 @@ sourceSets {
resources {
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 {
enabled = false
}
// Configure and enable bootJar for this project
bootJar {
enabled = true
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
zip64 = true
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
// Don't include all dependencies directly like the old jar task did
// 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 {
attributes(
'Main-Class': 'stirling.software.SPDF.SPDFApplication',
'Implementation-Title': 'Stirling-PDF',
'Implementation-Version': project.version
)
}
}
jar.dependsOn ':common:jar'
jar.dependsOn ':proprietary:jar'
bootJar.dependsOn ':common: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.boot.SpringApplication;
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.scheduling.annotation.EnableScheduling;
@ -39,10 +37,6 @@ import stirling.software.common.util.UrlUtils;
"stirling.software.SPDF",
"stirling.software.common",
"stirling.software.proprietary"
},
exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class
})
public class SPDFApplication {
@ -208,17 +202,37 @@ public class SPDFApplication {
}
private static String[] getActiveProfile(String[] args) {
if (args == null) {
return new String[] {"default"};
}
for (String arg : args) {
if (arg.contains("spring.profiles.active")) {
return arg.substring(args[0].indexOf('=') + 1).split(", ");
// 1. Check for explicitly passed profiles
if (args != null) {
for (String arg : args) {
if (arg.startsWith("--spring.profiles.active=")) {
String[] provided = arg.substring(arg.indexOf('=') + 1).split(",");
if (provided.length > 0) {
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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
// Handler for external static resources
registry.addResourceHandler("/**")
.addResourceLocations(
"file:" + InstallationPathConfig.getStaticPath(),
"classpath:/static/"
);
"file:" + InstallationPathConfig.getStaticPath(), "classpath:/static/");
registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css/");
// .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.PDDocumentCatalog;
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.PDField;
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")
@Operation(
summary = "Merge multiple PDF files into one",
@ -124,6 +166,7 @@ public class MergeController {
PDDocument mergedDocument = null;
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
boolean generateToc = request.isGenerateToc();
try {
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
ByteArrayOutputStream baos = new ByteArrayOutputStream();
mergedDocument.save(baos);

View File

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

View File

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

View File

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

View File

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

View File

@ -16,8 +16,10 @@ 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.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.model.api.converters.EmlToPdfRequest;
import stirling.software.common.service.CustomPDFDocumentFactory;
@ -39,9 +41,9 @@ public class ConvertEmlToPDF {
summary = "Convert EML to PDF",
description =
"This endpoint converts EML (email) files to PDF format with extensive"
+ " customization options. Features include font settings, image constraints, display modes, attachment handling,"
+ " and HTML debug output. Input: EML file, Output: PDF"
+ " or HTML file. Type: SISO")
+ " customization options. Features include font settings, image constraints, display modes, attachment handling,"
+ " and HTML debug output. Input: EML file, Output: PDF"
+ " or HTML file. Type: SISO")
public ResponseEntity<byte[]> convertEmlToPdf(@ModelAttribute EmlToPdfRequest request) {
MultipartFile inputFile = request.getFileInput();
@ -94,7 +96,8 @@ public class ConvertEmlToPDF {
try {
byte[] pdfBytes =
EmlToPdf.convertEmlToPdf(
runtimePathConfig.getWeasyPrintPath(), // Use configured WeasyPrint path
runtimePathConfig
.getWeasyPrintPath(), // Use configured WeasyPrint path
request,
fileBytes,
originalFilename,
@ -119,12 +122,20 @@ public class ConvertEmlToPDF {
.body("Conversion was interrupted".getBytes(StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
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)
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
} catch (RuntimeException e) {
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)
.body(errorMessage.getBytes(StandardCharsets.UTF_8));
}

View File

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

View File

@ -1,8 +1,5 @@
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.image.BufferedImage;
import java.io.ByteArrayOutputStream;
@ -20,17 +17,14 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
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.pdmodel.PDDocument;
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.RestController;
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.model.api.misc.OptimizePdfRequest;
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.PipelineResult;
import stirling.software.SPDF.service.ApiDocService;
import stirling.software.common.service.PostHogService;
import stirling.software.common.configuration.RuntimePathConfig;
import stirling.software.common.service.PostHogService;
import stirling.software.common.util.FileMonitor;
@Service

View File

@ -184,7 +184,8 @@ public class RedactController {
String pageNumbersInput = request.getPageNumbers();
String[] parsedPageNumbers =
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);
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.tags.Tag;
import stirling.software.common.util.CheckProgramInstall;
@Controller

View File

@ -130,6 +130,13 @@ public class GeneralWebController {
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")
@Hidden
public String multiToolForm(Model model) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,4 +32,11 @@ public class MergePdfsRequest extends MultiplePDFFiles {
requiredMode = Schema.RequiredMode.REQUIRED,
defaultValue = "true")
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 {
@Schema(
description =
"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.",
requiredMode = Schema.RequiredMode.REQUIRED)
description =
"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.",
requiredMode = Schema.RequiredMode.REQUIRED)
private MultipartFile[] overlayFiles;
@Schema(
description =
"The mode of overlaying: 'SequentialOverlay' for sequential application,"
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
+ " for fixed repetition based on provided counts",
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
requiredMode = Schema.RequiredMode.REQUIRED)
description =
"The mode of overlaying: 'SequentialOverlay' for sequential application,"
+ " 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay'"
+ " for fixed repetition based on provided counts",
allowableValues = {"SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"},
requiredMode = Schema.RequiredMode.REQUIRED)
private String overlayMode;
@Schema(
description =
"An array of integers specifying the number of times each corresponding overlay"
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should"
+ " match the length of the overlayFiles array.",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description =
"An array of integers specifying the number of times each corresponding overlay"
+ " file should be applied in the 'FixedRepeatOverlay' mode. This should"
+ " match the length of the overlayFiles array.",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private int[] counts;
@Schema(
description = "Overlay position 0 is Foregound, 1 is Background",
allowableValues = {"0", "1"},
requiredMode = Schema.RequiredMode.REQUIRED,
type = "number")
description = "Overlay position 0 is Foregound, 1 is Background",
allowableValues = {"0", "1"},
requiredMode = Schema.RequiredMode.REQUIRED,
type = "number")
private int overlayPosition;
}

View File

@ -14,9 +14,9 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
public class AddStampRequest extends PDFWithPageNums {
@Schema(
description = "The stamp type (text or image)",
allowableValues = {"text", "image"},
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The stamp type (text or image)",
allowableValues = {"text", "image"},
requiredMode = Schema.RequiredMode.REQUIRED)
private String stampType;
@Schema(description = "The stamp text", defaultValue = "Stirling Software")
@ -26,60 +26,60 @@ public class AddStampRequest extends PDFWithPageNums {
private MultipartFile stampImage;
@Schema(
description = "The selected alphabet of the stamp text",
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
defaultValue = "roman")
description = "The selected alphabet of the stamp text",
allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"},
defaultValue = "roman")
private String alphabet = "roman";
@Schema(
description = "The font size of the stamp text and image",
defaultValue = "30",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The font size of the stamp text and image",
defaultValue = "30",
requiredMode = Schema.RequiredMode.REQUIRED)
private float fontSize;
@Schema(
description = "The rotation of the stamp in degrees",
defaultValue = "0",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The rotation of the stamp in degrees",
defaultValue = "0",
requiredMode = Schema.RequiredMode.REQUIRED)
private float rotation;
@Schema(
description = "The opacity of the stamp (0.0 - 1.0)",
defaultValue = "0.5",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The opacity of the stamp (0.0 - 1.0)",
defaultValue = "0.5",
requiredMode = Schema.RequiredMode.REQUIRED)
private float opacity;
@Schema(
description =
"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,"
+ " 7: top-left, 8: top-center, 9: top-right)",
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
defaultValue = "5",
requiredMode = Schema.RequiredMode.REQUIRED)
description =
"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,"
+ " 7: top-left, 8: top-center, 9: top-right)",
allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"},
defaultValue = "5",
requiredMode = Schema.RequiredMode.REQUIRED)
private int position;
@Schema(
description =
"Override X coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.",
defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED)
description =
"Override X coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.",
defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED)
private float overrideX; // Default to -1 indicating no override
@Schema(
description =
"Override Y coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.",
defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED)
description =
"Override Y coordinate for stamp placement. If set, it will override the"
+ " position-based calculation. Negative value means no override.",
defaultValue = "-1",
requiredMode = Schema.RequiredMode.REQUIRED)
private float overrideY; // Default to -1 indicating no override
@Schema(
description = "Specifies the margin size for the stamp.",
allowableValues = {"small", "medium", "large", "x-large"},
defaultValue = "medium",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "Specifies the margin size for the stamp.",
allowableValues = {"small", "medium", "large", "x-large"},
defaultValue = "medium",
requiredMode = Schema.RequiredMode.REQUIRED)
private String customMargin;
@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 {
@Schema(
description = "Delete all metadata if set to true",
defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "Delete all metadata if set to true",
defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean deleteAll;
@Schema(
description = "The author of the document",
defaultValue = "author",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The author of the document",
defaultValue = "author",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String author;
@Schema(
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The creation date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String creationDate;
@Schema(
description = "The creator of the document",
defaultValue = "creator",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The creator of the document",
defaultValue = "creator",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String creator;
@Schema(
description = "The keywords for the document",
defaultValue = "keywords",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The keywords for the document",
defaultValue = "keywords",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String keywords;
@Schema(
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The modification date of the document (format: yyyy/MM/dd HH:mm:ss)",
pattern = "yyyy/MM/dd HH:mm:ss",
defaultValue = "2023/10/01 12:00:00",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String modificationDate;
@Schema(
description = "The producer of the document",
defaultValue = "producer",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The producer of the document",
defaultValue = "producer",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String producer;
@Schema(
description = "The subject of the document",
defaultValue = "subject",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The subject of the document",
defaultValue = "subject",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String subject;
@Schema(
description = "The title of the document",
defaultValue = "title",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The title of the document",
defaultValue = "title",
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String title;
@Schema(
description = "The trapped status of the document",
defaultValue = "False",
allowableValues = {"True", "False", "Unknown"},
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
description = "The trapped status of the document",
defaultValue = "False",
allowableValues = {"True", "False", "Unknown"},
requiredMode = Schema.RequiredMode.NOT_REQUIRED)
private String trapped;
@Schema(
description =
"Map list of key and value of custom parameters. Note these must start with"
+ " customKey and customValue if they are non-standard")
description =
"Map list of key and value of custom parameters. Note these must start with"
+ " customKey and customValue if they are non-standard")
private Map<String, String> allRequestParams;
}

View File

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

View File

@ -12,24 +12,24 @@ import stirling.software.common.model.api.PDFFile;
public class AddPasswordRequest extends PDFFile {
@Schema(
description =
"The owner password to be added to the PDF file (Restricts what can be done"
+ " with the document once it is opened)",
format = "password")
description =
"The owner password to be added to the PDF file (Restricts what can be done"
+ " with the document once it is opened)",
format = "password")
private String ownerPassword;
@Schema(
description =
"The password to be added to the PDF file (Restricts the opening of the"
+ " document itself.)",
format = "password")
description =
"The password to be added to the PDF file (Restricts the opening of the"
+ " document itself.)",
format = "password")
private String password;
@Schema(
description = "The length of the encryption key",
allowableValues = {"40", "128", "256"},
defaultValue = "256",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The length of the encryption key",
allowableValues = {"40", "128", "256"},
defaultValue = "256",
requiredMode = Schema.RequiredMode.REQUIRED)
private int keyLength = 256;
@Schema(description = "Whether document assembly is prevented", defaultValue = "false")
@ -39,8 +39,8 @@ public class AddPasswordRequest extends PDFFile {
private Boolean preventExtractContent;
@Schema(
description = "Whether content extraction for accessibility is prevented",
defaultValue = "false")
description = "Whether content extraction for accessibility is prevented",
defaultValue = "false")
private Boolean preventExtractForAccessibility;
@Schema(description = "Whether form filling is prevented", defaultValue = "false")
@ -50,8 +50,8 @@ public class AddPasswordRequest extends PDFFile {
private Boolean preventModify;
@Schema(
description = "Whether modification of annotations is prevented",
defaultValue = "false")
description = "Whether modification of annotations is prevented",
defaultValue = "false")
private Boolean preventModifyAnnotations;
@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)
public class ManualRedactPdfRequest extends PDFWithPageNums {
@Schema(
description = "A list of areas that should be redacted",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "A list of areas that should be redacted",
requiredMode = Schema.RequiredMode.REQUIRED)
private List<RedactionArea> redactions;
@Schema(
description = "Convert the redacted PDF to an image",
defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "Convert the redacted PDF to an image",
defaultValue = "false",
requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean convertPDFToImage;
@Schema(
description = "The color used to fully redact certain pages",
defaultValue = "#000000",
requiredMode = Schema.RequiredMode.REQUIRED)
description = "The color used to fully redact certain pages",
defaultValue = "#000000",
requiredMode = Schema.RequiredMode.REQUIRED)
private String pageRedactionColor;
}

View File

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

View File

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

View File

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

View File

@ -29,13 +29,11 @@ spring.thymeleaf.encoding=UTF-8
spring.web.resources.mime-mappings.webmanifest=application/manifest+json
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.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.enabled=false
spring.jpa.hibernate.ddl-auto=update
server.servlet.session.timeout:30m
# 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
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
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)
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.fontName=Font Name
pdfPrompt=Select PDF(s)
@ -87,6 +219,12 @@ addToDoc=Add to Document
reset=Reset
apply=Apply
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.terms=Terms and Conditions
@ -127,6 +265,7 @@ enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
enterpriseEdition.proTeamFeatureDisabled=Team management features require a Pro licence or higher
#################
@ -207,6 +346,8 @@ account.property=Property
account.webBrowserSettings=Web Browser Setting
account.syncToBrowser=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
@ -238,6 +379,39 @@ adminUserSettings.disabledUsers=Disabled Users:
adminUserSettings.totalUsers=Total Users:
adminUserSettings.lastRequest=Last Request
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.header=Endpoint Statistics
@ -1026,6 +1200,7 @@ merge.header=Merge multiple PDFs (2+)
merge.sortByName=Sort by name
merge.sortByDate=Sort by date
merge.removeCertSign=Remove digital signature in the merged file?
merge.generateToc=Generate table of contents in the merged file?
merge.submit=Merge
@ -1475,3 +1650,57 @@ cookieBanner.preferencesModal.necessary.description=These cookies are essential
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.
#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)
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.fontName=Font Name
pdfPrompt=Select PDF(s)
@ -127,6 +259,7 @@ enterpriseEdition.button=Upgrade to Pro
enterpriseEdition.warning=This feature is only available to Pro users.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supports YAML configuration files and other SSO features.
enterpriseEdition.ssoAdvert=Looking for more user management features? Check out Stirling PDF Pro
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.discordSubmit=Discord - Submit Support post
#remove-image
removeImage.title=Remove image
removeImage.header=Remove image
removeImage.removeImage=Remove image
removeImage.submit=Remove image
#split-by-chapters
splitByChapters.title=Split PDF by Chapters
splitByChapters.header=Split PDF by Chapters
splitByChapters.bookmarkLevel=Bookmark Level
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner #
####################
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.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
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 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.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
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.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.necessary.title.1=Strictly Necessary Cookies
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.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)
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.fontName=Font Name
pdfPrompt=Hautatu PDFa(k)
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner #
####################
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.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
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 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.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
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.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.necessary.title.1=Strictly Necessary Cookies
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.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)
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.fontName=Nama Fonta
pdfPrompt=Pilih PDF
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner #
####################
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.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
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 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.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
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.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.necessary.title.1=Strictly Necessary Cookies
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.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)
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.fontName=Nome del font
pdfPrompt=Scegli PDF
multiPdfPrompt=Scegli 2 o più PDF
multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF
multiPdfPrompt=Scegli 2 o più PDF
multiPdfDropPrompt=Scegli (o trascina e rilascia) uno o più PDF
imgPrompt=Scegli immagine/i
genericSubmit=Invia
uploadLimit=Dimensione massima del file:
uploadLimitExceededSingular=è troppo grande. La dimensione massima consentita è
uploadLimitExceededPlural=sono troppo grandi. La dimensione massima consentita è
uploadLimitExceededSingular=è troppo grande. 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
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) :
@ -37,12 +169,12 @@ sizes.small=Piccolo
sizes.medium=Medio
sizes.large=Grande
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
username=Nome utente
password=Password
welcome=Benvenuto
property=Proprietà
property=Proprietà
black=Nero
white=Bianco
red=Rosso
@ -56,18 +188,18 @@ no=No
changedCredsMessage=Credenziali modificate!
notAuthenticatedMessage=Utente non autenticato.
userNotFoundMessage=Utente non trovato.
incorrectPasswordMessage=La password attuale non è corretta.
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.
incorrectPasswordMessage=La password attuale non è corretta.
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.
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.
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
disabledCurrentUserMessage=L'utente corrente non può essere disabilitato
downgradeCurrentUserLongMessage=Impossibile declassare il ruolo dell'utente corrente. Pertanto, l'utente corrente non verrà visualizzato.
userAlreadyExistsOAuthMessage=L'utente esiste già come utente OAuth2.
userAlreadyExistsWebMessage=L'utente esiste già come utente web.
disabledCurrentUserMessage=L'utente corrente non può essere disabilitato
downgradeCurrentUserLongMessage=Impossibile declassare il ruolo dell'utente corrente. Pertanto, l'utente corrente non verrà visualizzato.
userAlreadyExistsOAuthMessage=L'utente esiste già come utente OAuth2.
userAlreadyExistsWebMessage=L'utente esiste già come utente web.
error=Errore
oops=Oops!
help=Aiuto
@ -90,7 +222,7 @@ noFileSelected=Nessun file selezionato. Caricane uno.
legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni
legal.accessibility=Accessibilità
legal.accessibility=Accessibilità
legal.cookie=Informativa sui cookie
legal.impressum=Informazioni legali
legal.showCookieBanner=Preferenze sui cookie
@ -98,7 +230,7 @@ legal.showCookieBanner=Preferenze sui cookie
###############
# Pipeline #
###############
pipeline.header=Menù pipeline (Beta)
pipeline.header=Menù pipeline (Beta)
pipeline.uploadButton=Caricamento personalizzato
pipeline.configureButton=Configura
pipeline.defaultOption=Personalizzato
@ -124,9 +256,9 @@ pipelineOptions.validateButton=Convalidare
# ENTERPRISE EDITION #
########################
enterpriseEdition.button=Aggiorna alla versione Pro
enterpriseEdition.warning=Questa funzionalità è disponibile solo per gli utenti Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supporta i file di configurazione YAML e altre funzionalità SSO.
enterpriseEdition.ssoAdvert=Cerchi altre funzionalità di gestione degli utenti? Dai un'occhiata a Stirling PDF Pro
enterpriseEdition.warning=Questa funzionalità è disponibile solo per gli utenti Pro.
enterpriseEdition.yamlAdvert=Stirling PDF Pro supporta i file di configurazione YAML e altre funzionalità SSO.
enterpriseEdition.ssoAdvert=Cerchi altre funzionalità di gestione degli utenti? Dai un'occhiata a Stirling PDF Pro
#################
@ -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.enable=Abilita 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.recent=Nuovo e aggiornato di recente
navbar.darkmode=Modalità Scura
navbar.darkmode=Modalità Scura
navbar.language=Lingue
navbar.settings=Impostazioni
navbar.allTools=Strumenti
@ -164,7 +296,7 @@ navbar.sections.popular=Popolare
#############
settings.title=Impostazioni
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.downloadOption.title=Scegli opzione di download (Per file singoli non compressi):
settings.downloadOption.1=Apri in questa finestra
@ -203,7 +335,7 @@ account.signOut=Logout
account.yourApiKey=La tua API Key
account.syncTitle=Sincronizza le impostazioni del browser con l'account
account.settingsCompare=Confronto delle impostazioni:
account.property=Proprietà
account.property=Proprietà
account.webBrowserSettings=Impostazione del browser web
account.syncToBrowser=Sincronizza account -> Browser
account.syncToAccount=Sincronizza account <- Browser
@ -217,7 +349,7 @@ adminUserSettings.addUser=Aggiungi un nuovo Utente
adminUserSettings.deleteUser=Elimina utente
adminUserSettings.confirmDeleteUser=L'utente deve essere eliminato?
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.role=Ruolo
adminUserSettings.actions=Azioni
@ -272,24 +404,24 @@ database.deleteBackupFile=Elimina file di backup
database.importBackupFile=Importa file di backup
database.createBackupFile=Crea 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_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_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.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.fileNotFound=File non trovato
database.fileNullOrEmpty=Il file non deve essere nullo o vuoto
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
#############
# HOME-PAGE #
#############
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
@ -302,7 +434,7 @@ home.showFavorites=Mostra preferiti
home.legacyHomepage=Vecchia homepage
home.newHomePage=Prova la nostra nuova homepage!
home.alphabetical=Alfabetico
home.globalPopularity=Popolarità
home.globalPopularity=Popolarità
home.sortBy=Ordinamento:
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
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
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
home.rotate.title=Ruota
@ -365,11 +497,11 @@ home.compressPdfs.desc=Comprimi PDF per ridurne le dimensioni.
compressPdfs.tags=comprimere,piccolo,minuscolo
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
home.changeMetadata.title=Modifica Proprietà
home.changeMetadata.desc=Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.
home.changeMetadata.title=Modifica Proprietà
home.changeMetadata.desc=Modifica/Aggiungi/Rimuovi le proprietà di un documento PDF.
changeMetadata.tags=Titolo,autore,data,creazione,ora,editore,produttore,statistiche
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.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.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
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
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
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
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
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
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
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.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
@ -556,7 +688,7 @@ home.overlay-pdfs.desc=Sovrappone i PDF sopra un altro PDF
overlay-pdfs.tags=Sovrapponi
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
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.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
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.header=Sostituisci-Inverti colore PDF
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
replace-color.selectText.1=Sostituisci o inverti le opzioni del colore
replace-color.selectText.2=Predefinito (colori ad alto contrasto predefiniti)
@ -609,11 +741,11 @@ login.header=Accedi
login.signin=Accedi
login.rememberme=Ricordami
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.ssoSignIn=Accedi tramite Single Sign-on
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.oauth2InvalidUserInfoResponse=Risposta relativa alle informazioni utente non valida
login.oauth2invalidRequest=Richiesta non valida
@ -621,8 +753,8 @@ login.oauth2AccessDenied=Accesso negato
login.oauth2InvalidTokenResponse=Risposta token non valida
login.oauth2InvalidIdToken=Id Token non valido
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.alreadyLoggedIn=Hai già effettuato l'accesso a
login.userIsDisabled=L'utente è disattivato, l'accesso è attualmente bloccato con questo nome utente. Si prega di contattare l'amministratore.
login.alreadyLoggedIn=Hai già effettuato l'accesso a
login.alreadyLoggedIn2=dispositivi. Esci dai dispositivi e riprova.
login.toManySessions=Hai troppe sessioni attive
login.logoutMessage=Sei stato disconnesso.
@ -692,22 +824,22 @@ getPdfInfo.header=Ottieni informazioni in PDF
getPdfInfo.submit=Ottieni informazioni
getPdfInfo.downloadJson=Scarica JSON
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.compliance=Questo PDF è conforme allo standard {0}
getPdfInfo.summary.compliance=Questo PDF è conforme allo standard {0}
getPdfInfo.summary.basicInfo=Informazioni di base
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.permissions.alert=Autorizzazioni limitate: {0} azioni non sono consentite
getPdfInfo.summary.all.permissions.alert=Tutti i permessi consentiti
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.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.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.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
@ -766,7 +898,7 @@ AddStampRequest.stampImage=Immagine del timbro
AddStampRequest.alphabet=Alfabeto
AddStampRequest.fontSize=Dimensione carattere/immagine
AddStampRequest.rotation=Rotazione
AddStampRequest.opacity=Opacità
AddStampRequest.opacity=Opacità
AddStampRequest.position=Posizione
AddStampRequest.overrideX=Sostituisci la coordinata X
AddStampRequest.overrideY=Sostituisci la coordinata Y
@ -798,7 +930,7 @@ addPageNumbers.selectText.5=Pagine da numerare
addPageNumbers.selectText.6=Testo personalizzato
addPageNumbers.customTextDesc=Testo personalizzato
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
@ -812,7 +944,7 @@ auto-rename.submit=Rinomina automatica
adjustContrast.title=Regola il contrasto
adjustContrast.header=Regola il contrasto
adjustContrast.contrast=Contrasto:
adjustContrast.brightness=Luminosità:
adjustContrast.brightness=Luminosità:
adjustContrast.saturation=Saturazione:
adjustContrast.download=Download
@ -826,13 +958,13 @@ crop.submit=Invia
#autoSplitPDF
autoSplitPDF.title=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.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.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.duplexMode=Modalità duplex (scansione fronte e retro)
autoSplitPDF.duplexMode=Modalità duplex (scansione fronte e retro)
autoSplitPDF.dividerDownload2=Scarica 'Divisore automatico (con istruzioni).pdf'
autoSplitPDF.submit=Invia
@ -862,7 +994,7 @@ scalePages.submit=Invia
certSign.title=Firma del certificato
certSign.header=Firma un PDF con il tuo certificato (Lavoro in corso)
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.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):
@ -950,7 +1082,7 @@ flatten.submit=Appiattisci
#ScannerImageSplit
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.4=Imposta lo spettro di colori attorno al colore di sfondo stimato (default: 30).
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.9=Spessore bordo:
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
@ -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.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.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.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.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.credit=Questo servizio utilizza Qpdf e Tesseract per l'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.grayscale.label=Applica scala di grigio per la 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.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.submit=Comprimi
@ -1022,7 +1154,7 @@ addImage.submit=Aggiungi immagine
#merge
merge.title=Unisci
merge.header=Unisci 2 o più PDF
merge.header=Unisci 2 o più PDF
merge.sortByName=Ordina per nome
merge.sortByDate=Ordina per data
merge.removeCertSign=Rimuovere la firma digitale nel file unito?
@ -1033,7 +1165,7 @@ merge.submit=Unisci
pdfOrganiser.title=Organizza pagine
pdfOrganiser.header=Organizza le pagine di un PDF
pdfOrganiser.submit=Riordina pagine
pdfOrganiser.mode=Modalità
pdfOrganiser.mode=Modalità
pdfOrganiser.mode.1=Ordine delle pagine personalizzato
pdfOrganiser.mode.2=Ordine inverso
pdfOrganiser.mode.3=Ordinamento fronte-retro
@ -1074,17 +1206,17 @@ multiTool.undo=Annulla
multiTool.redo=Rifai
#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.noPassword=Nessuna password fornita per il PDF crittografato: {0}
decrypt.invalidPassword=Riprova con la password corretta.
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.success=File decrittografato con successo.
#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
viewPdf.title=Visualizza/Modifica PDF
@ -1129,7 +1261,7 @@ imageToPDF.fillPage=Riempi la pagina
imageToPDF.fitDocumentToImage=Adatta la pagina all'immagine
imageToPDF.maintainAspectRatio=Mantieni le proporzioni
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.5=Converti in PDF separati
@ -1140,13 +1272,13 @@ pdfToImage.header=PDF a immagine
pdfToImage.selectText=Formato immagini
pdfToImage.singleOrMultiple=Tipo di immagine
pdfToImage.single=Unica immagine larga
pdfToImage.multi=Più immagini
pdfToImage.multi=Più immagini
pdfToImage.colorType=Tipo di colore
pdfToImage.color=A colori
pdfToImage.grey=Scala di grigi
pdfToImage.blackwhite=Bianco e Nero (potresti perdere dettagli!)
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)
@ -1156,11 +1288,11 @@ addPassword.header=Aggiungi password (crittografa)
addPassword.selectText.1=Seleziona PDF da crittografare
addPassword.selectText.2=Password
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.6=Previeni assemblaggio del documento
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.10=Previeni modifiche
addPassword.selectText.11=Previeni annotazioni
@ -1182,7 +1314,7 @@ watermark.selectText.3=Dimensione carattere:
watermark.selectText.4=Rotazione (0-360):
watermark.selectText.5=spazio orizzontale (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.9=Immagine filigrana:
watermark.selectText.10=Converti PDF in PDF-Immagine
@ -1194,12 +1326,12 @@ watermark.type.2=Immagine
#Change permissions
permissions.title=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.2=Permessi da impostare
permissions.selectText.3=Previeni assemblaggio del documento
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.7=Previeni modifiche
permissions.selectText.8=Previeni annotazioni
@ -1218,10 +1350,10 @@ removePassword.submit=Rimuovi Password
#changeMetadata
changeMetadata.title=Titolo:
changeMetadata.header=Cambia Proprietà
changeMetadata.header=Cambia Proprietà
changeMetadata.selectText.1=Imposta i dati che vuoi cambiare
changeMetadata.selectText.2=Cancella tutte le proprietà
changeMetadata.selectText.3=Visualizza proprietà personalizzate:
changeMetadata.selectText.2=Cancella tutte le proprietà
changeMetadata.selectText.3=Visualizza proprietà personalizzate:
changeMetadata.author=Autore:
changeMetadata.creationDate=Data di creazione (yyyy/MM/dd HH:mm:ss):
changeMetadata.creator=Creatore:
@ -1230,9 +1362,9 @@ changeMetadata.modDate=Data di modifica (yyyy/MM/dd HH:mm:ss):
changeMetadata.producer=Produttore:
changeMetadata.subject=Oggetto:
changeMetadata.trapped=Recuperato:
changeMetadata.selectText.4=Altre proprietà:
changeMetadata.selectText.5=Aggiungi proprietà personalizzata:
changeMetadata.submit=Cambia proprietà
changeMetadata.selectText.4=Altre proprietà:
changeMetadata.selectText.5=Aggiungi proprietà personalizzata:
changeMetadata.submit=Cambia proprietà
#unlockPDFForms
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.credit=Questo servizio utilizza libreoffice per la conversione in PDF/A.
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.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
@ -1308,11 +1440,11 @@ split-by-size-or-count.submit=Separa
overlay-pdfs.header=Invia file PDF in sovrapposizione
overlay-pdfs.baseFile.label=Seleziona File PDF di base
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.interleaved=Sovrapposizione interfogliata
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.position.label=Seleziona posizione di sovrapposizione
overlay-pdfs.position.foreground=Primo piano
@ -1351,16 +1483,16 @@ licenses.license=Licenza
survey.nav=Sondaggio
survey.title=Sondaggio Stirling-PDF
survey.description=Stirling-PDF non fa tracciamento, quindi vogliamo sentire i nostri utenti per migliorare Stirling-PDF!
survey.changes=Stirling-PDF è cambiato dall'ultimo sondaggio! Per saperne di più, consulta il nostro blog qui:
survey.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.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.please=Ti invitiamo a prendere in considerazione la possibilità di partecipare al nostro sondaggio!
survey.disabled=(Il popup del sondaggio verrà disabilitato nei prossimi aggiornamenti ma sarà disponibile a piè di pagina)
survey.button=Partecipa al sondaggio
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.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.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.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!
@ -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.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.2=Qualcosa è andato storto
error.404.2=Qualcosa è andato storto
error.github=Apri un ticket su GitHub
error.showStack=Mostra traccia dello stack
error.copyStack=Copia traccia dello stack
@ -1393,10 +1525,10 @@ splitByChapters.header=Dividi PDF per capitoli
splitByChapters.bookmarkLevel=Livello segnalibro
splitByChapters.includeMetadata=Includi Metadati
splitByChapters.allowDuplicates=Consenti duplicati
splitByChapters.desc.1=Questo strumento divide un file PDF in più PDF in base alla struttura dei capitoli.
splitByChapters.desc.1=Questo strumento divide un file PDF in più PDF in base alla struttura dei capitoli.
splitByChapters.desc.2=Livello segnalibro: seleziona il livello dei segnalibri da utilizzare per la suddivisione (0 per il livello superiore, 1 per il secondo livello, ecc.).
splitByChapters.desc.3=Includi metadati: se selezionato, i metadati del PDF originale verranno inclusi in ogni PDF diviso.
splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
splitByChapters.desc.4=Consenti duplicati: se selezionata, consente più segnalibri sulla stessa pagina per creare PDF separati.
splitByChapters.submit=Dividi PDF
#File Chooser
@ -1429,13 +1561,13 @@ validateSignature.location=Posizione
validateSignature.noSignatures=Nessuna firma digitale trovata in questo documento
validateSignature.status.valid=Valida
validateSignature.status.invalid=Invalida
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.cert.expired=Il certificato è scaduto
validateSignature.cert.revoked=Il certificato è stato revocato
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.cert.expired=Il certificato è scaduto
validateSignature.cert.revoked=Il certificato è stato revocato
validateSignature.signature.info=Informazioni sulla 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.cert.info=Dettagli del certificato
validateSignature.cert.issuer=Emittente
@ -1454,7 +1586,7 @@ validateSignature.cert.bits=bit
# Cookie banner #
####################
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.acceptAllBtn=Acconsento
cookieBanner.popUp.acceptNecessaryBtn=No grazie
@ -1466,12 +1598,47 @@ cookieBanner.preferencesModal.savePreferencesBtn=Salva preferenze
cookieBanner.preferencesModal.closeIconLabel=Chiusura modale
cookieBanner.preferencesModal.serviceCounterLabel=Servizio|Servizi
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.2=Stirling PDF non può e non potrà mai tracciare o accedere al contenuto dei documenti che utilizzi.
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.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.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.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)
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.fontName=Lettertypenaam
pdfPrompt=Selecteer PDF('s)
@ -164,7 +296,7 @@ navbar.sections.popular=Popular
#############
settings.title=Instellingen
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.downloadOption.title=Kies download optie (Voor enkelvoudige bestanddownloads zonder zip):
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
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
home.split.title=Splitsen
@ -446,7 +578,7 @@ home.removeCertSign.desc=Verwijder certificaat van PDF
removeCertSign.tags=authenticeren,PEM,P12,officieel,ontsleutelen
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
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
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
home.add-page-numbers.title=Paginanummers toevoegen
@ -524,13 +656,13 @@ home.extractPage.desc=Extraheert geselecteerde pagina's uit PDF
extractPage.tags=extraheren
home.PdfToSinglePage.title=PDF naar één grote pagina
home.PdfToSinglePage.desc=Voegt alle PDF-pagina's samen tot één grote pagina
PdfToSinglePage.tags=één pagina
home.PdfToSinglePage.title=PDF naar één grote pagina
home.PdfToSinglePage.desc=Voegt alle PDF-pagina's samen tot één grote pagina
PdfToSinglePage.tags=één pagina
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
home.autoRedact.title=Automatisch censureren
@ -768,8 +900,8 @@ AddStampRequest.fontSize=Tekst/afbeelding grootte
AddStampRequest.rotation=Rotatie
AddStampRequest.opacity=Transparantie
AddStampRequest.position=Positie
AddStampRequest.overrideX=X coördinaat overschrijven
AddStampRequest.overrideY=Y coördinaat overschrijven
AddStampRequest.overrideX=X coördinaat overschrijven
AddStampRequest.overrideY=Y coördinaat overschrijven
AddStampRequest.customMargin=Aangepaste marge
AddStampRequest.customColor=Aangepaste tekstkleur
AddStampRequest.submit=Indienen
@ -863,12 +995,12 @@ certSign.title=Certificaat ondertekening
certSign.header=Onderteken een PDF met je certificaat (in ontwikkeling)
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.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.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.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.reason=Reden
certSign.location=Locatie
@ -908,8 +1040,8 @@ compare.highlightColor.2=Hervormingskleur 2:
compare.document.1=Document 1
compare.document.2=Document 2
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.large.file.message=Eén of beiden van de bijgewerkte documenten zijn te groot om verwerkt te worden.
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.no.text.message=Een of beide geselecteerde PDF-bestanden bevatten geen tekstinhoud. Kies a.u.b. PDF-bestanden met tekst voor vergelijking.
#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.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.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
@ -985,7 +1117,7 @@ ocr.submit=Verwerk PDF met OCR
#extractImages
extractImages.title=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.submit=Extraheer
@ -1130,7 +1262,7 @@ imageToPDF.fitDocumentToImage=Pagina passend maken voor afbeelding
imageToPDF.maintainAspectRatio=Beeldverhoudingen behouden
imageToPDF.selectText.2=PDF automatisch draaien
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
@ -1139,14 +1271,14 @@ pdfToImage.title=PDF naar afbeelding
pdfToImage.header=PDF naar afbeelding
pdfToImage.selectText=Afbeeldingsformaat
pdfToImage.singleOrMultiple=Resultaattype van pagina naar afbeelding
pdfToImage.single=Eén grote afbeelding die alle pagina's combineert
pdfToImage.multi=Meerdere afbeeldingen, één afbeelding per pagina
pdfToImage.single=Eén grote afbeelding die alle pagina's combineert
pdfToImage.multi=Meerdere afbeeldingen, één afbeelding per pagina
pdfToImage.colorType=Kleurtype
pdfToImage.color=Kleur
pdfToImage.grey=Grijstinten
pdfToImage.blackwhite=Zwart en wit (kan data verliezen!)
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)
@ -1310,7 +1442,7 @@ overlay-pdfs.baseFile.label=Selecteer basis PDF-bestand
overlay-pdfs.overlayFiles.label=Selecteer overlappende PDF-bestanden
overlay-pdfs.mode.label=Selecteer overlappingsmodus
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.counts.label=Aantal keren overlappen (voor vaste herhalings modus)
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.vertical.placeholder=Voer het aantal verticale secties in
split-by-sections.submit=PDF splitsen
split-by-sections.merge=Samenvoegen in één PDF
split-by-sections.merge=Samenvoegen in één PDF
#printFile
@ -1348,14 +1480,14 @@ licenses.version=Versie
licenses.license=Licentie
#survey
survey.nav=Enquête
survey.title=Stirling-PDF Enquête
survey.nav=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.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.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.button=Vul enquête in.
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.button=Vul enquête in.
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.2=This is a chance to:
@ -1454,8 +1586,8 @@ validateSignature.cert.bits=bits
# Cookie banner #
####################
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.2=If youd rather not, clicking 'No Thanks' will only enable the essential cookies needed to keep things running smoothly.
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 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.acceptNecessaryBtn=No Thanks
cookieBanner.popUp.showPreferencesBtn=Manage preferences
@ -1467,11 +1599,46 @@ cookieBanner.preferencesModal.closeIconLabel=Close modal
cookieBanner.preferencesModal.serviceCounterLabel=Service|Services
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.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.necessary.title.1=Strictly Necessary Cookies
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.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