mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-13 02:55:03 +00:00
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:
parent
bdc35519da
commit
1f2365f03c
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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 && \
|
||||
|
39
build.gradle
39
build.gradle
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -1,7 +1,9 @@
|
||||
repositories {
|
||||
maven { url = "https://build.shibboleth.net/maven/releases" }
|
||||
}
|
||||
|
||||
bootRun {
|
||||
enabled = false
|
||||
}
|
||||
dependencies {
|
||||
implementation project(':common')
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
@ -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 {}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
387
proprietary/src/main/resources/static/css/modern-tables.css
Normal file
387
proprietary/src/main/resources/static/css/modern-tables.css
Normal 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
|
||||
}
|
@ -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>
|
133
proprietary/src/main/resources/templates/accounts/teams.html
Normal file
133
proprietary/src/main/resources/templates/accounts/teams.html
Normal 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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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'
|
||||
|
2
stirling-pdf/.gitignore
vendored
2
stirling-pdf/.gitignore
vendored
@ -193,4 +193,4 @@ id_ed25519.pub
|
||||
**/jcef-bundle/
|
||||
|
||||
# node_modules
|
||||
node_modules/
|
||||
node_modules/
|
@ -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'
|
@ -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() {
|
||||
|
@ -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) {}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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")
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
1644
stirling-pdf/src/main/resources/messages_bo_CN.properties
Normal file
1644
stirling-pdf/src/main/resources/messages_bo_CN.properties
Normal file
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
@ -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
|
||||
|
||||
|
||||
|
@ -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 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.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 can’t 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 assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured\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
@ -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 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.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 can’t 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 assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
|
||||
#fakeScan
|
||||
fakeScan.title=Fake Scan
|
||||
fakeScan.header=Fake Scan
|
||||
fakeScan.description=Create a PDF that looks like it was scanned
|
||||
fakeScan.selectPDF=Select PDF:
|
||||
fakeScan.quality=Scan Quality
|
||||
fakeScan.quality.low=Low
|
||||
fakeScan.quality.medium=Medium
|
||||
fakeScan.quality.high=High
|
||||
fakeScan.rotation=Rotation Angle
|
||||
fakeScan.rotation.none=None
|
||||
fakeScan.rotation.slight=Slight
|
||||
fakeScan.rotation.moderate=Moderate
|
||||
fakeScan.rotation.severe=Severe
|
||||
fakeScan.submit=Create Fake Scan
|
||||
|
||||
#home.fakeScan
|
||||
home.fakeScan.title=Fake Scan
|
||||
home.fakeScan.desc=Create a PDF that looks like it was scanned
|
||||
fakeScan.tags=scan,simulate,realistic,convert
|
||||
|
||||
# FakeScan advanced settings (frontend)
|
||||
fakeScan.advancedSettings=Enable Advanced Scan Settings
|
||||
fakeScan.colorspace=Colorspace
|
||||
fakeScan.colorspace.grayscale=Grayscale
|
||||
fakeScan.colorspace.color=Color
|
||||
fakeScan.border=Border (px)
|
||||
fakeScan.rotate=Base Rotation (degrees)
|
||||
fakeScan.rotateVariance=Rotation Variance (degrees)
|
||||
fakeScan.brightness=Brightness
|
||||
fakeScan.contrast=Contrast
|
||||
fakeScan.blur=Blur
|
||||
fakeScan.noise=Noise
|
||||
fakeScan.yellowish=Yellowish (simulate old paper)
|
||||
fakeScan.resolution=Resolution (DPI)
|
||||
|
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
@ -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 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.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 can’t 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 assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
|
||||
#fakeScan
|
||||
fakeScan.title=Fake Scan
|
||||
fakeScan.header=Fake Scan
|
||||
fakeScan.description=Create a PDF that looks like it was scanned
|
||||
fakeScan.selectPDF=Select PDF:
|
||||
fakeScan.quality=Scan Quality
|
||||
fakeScan.quality.low=Low
|
||||
fakeScan.quality.medium=Medium
|
||||
fakeScan.quality.high=High
|
||||
fakeScan.rotation=Rotation Angle
|
||||
fakeScan.rotation.none=None
|
||||
fakeScan.rotation.slight=Slight
|
||||
fakeScan.rotation.moderate=Moderate
|
||||
fakeScan.rotation.severe=Severe
|
||||
fakeScan.submit=Create Fake Scan
|
||||
|
||||
#home.fakeScan
|
||||
home.fakeScan.title=Fake Scan
|
||||
home.fakeScan.desc=Create a PDF that looks like it was scanned
|
||||
fakeScan.tags=scan,simulate,realistic,convert
|
||||
|
||||
# FakeScan advanced settings (frontend)
|
||||
fakeScan.advancedSettings=Enable Advanced Scan Settings
|
||||
fakeScan.colorspace=Colorspace
|
||||
fakeScan.colorspace.grayscale=Grayscale
|
||||
fakeScan.colorspace.color=Color
|
||||
fakeScan.border=Border (px)
|
||||
fakeScan.rotate=Base Rotation (degrees)
|
||||
fakeScan.rotateVariance=Rotation Variance (degrees)
|
||||
fakeScan.brightness=Brightness
|
||||
fakeScan.contrast=Contrast
|
||||
fakeScan.blur=Blur
|
||||
fakeScan.noise=Noise
|
||||
fakeScan.yellowish=Yellowish (simulate old paper)
|
||||
fakeScan.resolution=Resolution (DPI)
|
||||
|
@ -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
1644
stirling-pdf/src/main/resources/messages_ml_IN.properties
Normal file
1644
stirling-pdf/src/main/resources/messages_ml_IN.properties
Normal file
File diff suppressed because it is too large
Load Diff
@ -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 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.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 can’t 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 assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured?Stirling PDF cannot and will never track the content of the documents you work with.
|
||||
|
||||
#fakeScan
|
||||
fakeScan.title=Fake Scan
|
||||
fakeScan.header=Fake Scan
|
||||
fakeScan.description=Create a PDF that looks like it was scanned
|
||||
fakeScan.selectPDF=Select PDF:
|
||||
fakeScan.quality=Scan Quality
|
||||
fakeScan.quality.low=Low
|
||||
fakeScan.quality.medium=Medium
|
||||
fakeScan.quality.high=High
|
||||
fakeScan.rotation=Rotation Angle
|
||||
fakeScan.rotation.none=None
|
||||
fakeScan.rotation.slight=Slight
|
||||
fakeScan.rotation.moderate=Moderate
|
||||
fakeScan.rotation.severe=Severe
|
||||
fakeScan.submit=Create Fake Scan
|
||||
|
||||
#home.fakeScan
|
||||
home.fakeScan.title=Fake Scan
|
||||
home.fakeScan.desc=Create a PDF that looks like it was scanned
|
||||
fakeScan.tags=scan,simulate,realistic,convert
|
||||
|
||||
# FakeScan advanced settings (frontend)
|
||||
fakeScan.advancedSettings=Enable Advanced Scan Settings
|
||||
fakeScan.colorspace=Colorspace
|
||||
fakeScan.colorspace.grayscale=Grayscale
|
||||
fakeScan.colorspace.color=Color
|
||||
fakeScan.border=Border (px)
|
||||
fakeScan.rotate=Base Rotation (degrees)
|
||||
fakeScan.rotateVariance=Rotation Variance (degrees)
|
||||
fakeScan.brightness=Brightness
|
||||
fakeScan.contrast=Contrast
|
||||
fakeScan.blur=Blur
|
||||
fakeScan.noise=Noise
|
||||
fakeScan.yellowish=Yellowish (simulate old paper)
|
||||
fakeScan.resolution=Resolution (DPI)
|
||||
|
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
Loading…
x
Reference in New Issue
Block a user