1
0
mirror of https://github.com/Stirling-Tools/Stirling-PDF.git synced 2024-11-19 22:00:11 +01:00

Merge branch 'main' into mannam11/file-override-fix-529

This commit is contained in:
Anthony Stirling 2023-12-30 11:24:45 +00:00 committed by GitHub
commit da50e4d212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 2152 additions and 984 deletions

4
.gitignore vendored
View File

@ -15,8 +15,8 @@ local.properties
.classpath .classpath
.project .project
version.properties version.properties
pipeline/ pipeline/watchedFolders/
pipeline/finishedFolders/
#### Stirling-PDF Files ### #### Stirling-PDF Files ###
customFiles/ customFiles/
configs/ configs/

View File

@ -6,7 +6,8 @@ ARG VERSION_TAG
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \ # PUID=1000 \
# PGID=1000 \ # PGID=1000 \
# UMASK=022 \ # UMASK=022 \
@ -18,13 +19,14 @@ ENV DOCKER_ENABLE_SECURITY=false \
## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME ## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
##&& \ ##&& \
## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original ## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
# Copy necessary files # Copy necessary files
COPY ./scripts/* /scripts/ COPY ./scripts/* /scripts/
COPY ./pipeline/ /pipeline/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
@ -42,4 +44,4 @@ EXPOSE 8080
# Set user and run command # Set user and run command
##USER stirlingpdfuser ##USER stirlingpdfuser
ENTRYPOINT ["/scripts/init.sh"] ENTRYPOINT ["/scripts/init.sh"]
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@ -17,7 +17,8 @@ RUN apt-get update && \
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \ # PUID=1000 \
# PGID=1000 \ # PGID=1000 \
# UMASK=022 \ # UMASK=022 \
@ -28,13 +29,14 @@ ENV DOCKER_ENABLE_SECURITY=false \
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles # chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
# Copy necessary files # Copy necessary files
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY ./pipeline/ /pipeline/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
@ -60,4 +62,4 @@ ENV DOCKER_ENABLE_SECURITY=false
# Run the application # Run the application
#USER stirlingpdfuser #USER stirlingpdfuser
ENTRYPOINT ["/scripts/init-without-ocr.sh"] ENTRYPOINT ["/scripts/init-without-ocr.sh"]
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@ -6,7 +6,8 @@ ARG VERSION_TAG
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \ # PUID=1000 \
# PGID=1000 \ # PGID=1000 \
# UMASK=022 \ # UMASK=022 \
@ -18,12 +19,12 @@ ENV DOCKER_ENABLE_SECURITY=false \
# Set up necessary directories and permissions # Set up necessary directories and permissions
#RUN mkdir -p /scripts /configs /customFiles && \ #RUN mkdir -p /scripts /configs /customFiles && \
# chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles # chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY ./pipeline/ /pipeline/
COPY build/libs/*.jar app.jar COPY build/libs/*.jar app.jar
# Set font cache and permissions # Set font cache and permissions
@ -42,4 +43,4 @@ ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
ENTRYPOINT ["/scripts/init-without-ocr.sh"] ENTRYPOINT ["/scripts/init-without-ocr.sh"]
# Run the application # Run the application
CMD ["java", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@ -147,7 +147,7 @@ Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "pod
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
## Want to add your own language? ## Want to add your own language?
Stirling PDF currently supports 20! Stirling PDF currently supports 21!
- English (English) (en_GB) - English (English) (en_GB)
- English (US) (en_US) - English (US) (en_US)
- Arabic (العربية) (ar_AR) - Arabic (العربية) (ar_AR)
@ -169,6 +169,7 @@ Stirling PDF currently supports 20!
- Greek (el_GR) - Greek (el_GR)
- Turkish (Türkçe) (tr_TR) - Turkish (Türkçe) (tr_TR)
- Indonesia (Bahasa Indonesia) (id_ID) - Indonesia (Bahasa Indonesia) (id_ID)
- Hindi (हिंदी) (hi_IN)
If you want to add your own language to Stirling-PDF please refer If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md

View File

@ -8,7 +8,7 @@ plugins {
} }
group = 'stirling.software' group = 'stirling.software'
version = '0.17.2' version = '0.18.1'
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
@ -48,7 +48,7 @@ launch4j {
errTitle="Encountered error, Do you have Java 17?" errTitle="Encountered error, Do you have Java 17?"
downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true"] variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
jreMinVersion="17" jreMinVersion="17"
mutexName="Stirling-PDF" mutexName="Stirling-PDF"
@ -68,17 +68,17 @@ dependencies {
implementation 'org.springframework:spring-webmvc:6.0.15' implementation 'org.springframework:spring-webmvc:6.0.15'
implementation 'org.yaml:snakeyaml:2.1' implementation 'org.yaml:snakeyaml:2.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.1.6' implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.6' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
implementation 'org.springframework.boot:spring-boot-starter-security:3.1.6' implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa" implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation "com.h2database:h2" implementation "com.h2database:h2"
} }
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.6' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
// Batik // Batik
implementation 'org.apache.xmlgraphics:batik-all:1.17' implementation 'org.apache.xmlgraphics:batik-all:1.17'

View File

@ -0,0 +1,39 @@
{
"name": "Prepare-pdfs-for-email",
"pipeline": [
{
"operation": "/api/v1/misc/repair",
"parameters": {}
},
{
"operation": "/api/v1/security/sanitize-pdf",
"parameters": {
"removeJavaScript": true,
"removeEmbeddedFiles": false,
"removeMetadata": false,
"removeLinks": false,
"removeFonts": false
}
},
{
"operation": "/api/v1/misc/compress-pdf",
"parameters": {
"optimizeLevel": 2,
"expectedOutputSize": ""
}
},
{
"operation": "/api/v1/general/split-by-size-or-count",
"parameters": {
"splitType": 0,
"splitValue": "15MB"
}
}
],
"_examples": {
"outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
},
"outputDir": "httpWebRequest",
"outputFileName": "{filename}"
}

View File

@ -0,0 +1,33 @@
{
"name": "split-rotate-auto-rename",
"pipeline": [
{
"operation": "/api/v1/general/split-pdf-by-sections",
"parameters": {
"horizontalDivisions": 2,
"verticalDivisions": 2,
"fileInput": "automated"
}
},
{
"operation": "/api/v1/general/rotate-pdf",
"parameters": {
"angle": 90,
"fileInput": "automated"
}
},
{
"operation": "/api/v1/misc/auto-rename",
"parameters": {
"useFirstTextAsFallback": false,
"fileInput": "automated"
}
}
],
"_examples": {
"outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}"
},
"outputDir": "{outputFolder}",
"outputFileName": "{filename}"
}

View File

@ -8,15 +8,16 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.EnableScheduling;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.config.ConfigInitializer;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@SpringBootApplication @SpringBootApplication
//@EnableScheduling @EnableScheduling
public class SPdfApplication { public class SPdfApplication {
@Autowired @Autowired
private Environment env; private Environment env;
@ -28,11 +29,7 @@ public class SPdfApplication {
if (browserOpen) { if (browserOpen) {
try { try {
String port = env.getProperty("local.server.port"); String url = "http://localhost:" + getPort();
if(port == null || port.length() == 0) {
port="8080";
}
String url = "http://localhost:" + port;
String os = System.getProperty("os.name").toLowerCase(); String os = System.getProperty("os.name").toLowerCase();
Runtime rt = Runtime.getRuntime(); Runtime rt = Runtime.getRuntime();
@ -45,7 +42,7 @@ public class SPdfApplication {
} }
} }
} }
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication app = new SpringApplication(SPdfApplication.class); SpringApplication app = new SpringApplication(SPdfApplication.class);
app.addInitializers(new ConfigInitializer()); app.addInitializers(new ConfigInitializer());
@ -55,28 +52,28 @@ public class SPdfApplication {
System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); System.out.println("External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
} }
app.run(args); app.run(args);
try { try {
Thread.sleep(1000); Thread.sleep(1000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
// TODO Auto-generated catch block // TODO Auto-generated catch block
e.printStackTrace(); e.printStackTrace();
} }
GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/static/");
GeneralUtils.createDir("customFiles/templates/"); GeneralUtils.createDir("customFiles/templates/");
System.out.println("Stirling-PDF Started."); System.out.println("Stirling-PDF Started.");
String port = System.getProperty("local.server.port"); String url = "http://localhost:" + getPort();
if(port == null || port.length() == 0) {
port="8080";
}
String url = "http://localhost:" + port;
System.out.println("Navigate to " + url); System.out.println("Navigate to " + url);
} }
public static String getPort() {
} String port = System.getProperty("local.server.port");
if (port == null || port.isEmpty()) {
port = "8080";
}
return port;
}
}

View File

@ -41,12 +41,16 @@ public class AppConfig {
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
} }
@Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() {
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null ? applicationProperties.getSystem().getEnableAlphaFunctionality() : false;
}
@Bean(name = "rateLimit") @Bean(name = "rateLimit")
public boolean rateLimit() { public boolean rateLimit() {
String appName = System.getProperty("rateLimit"); String appName = System.getProperty("rateLimit");
if (appName == null) if (appName == null)
appName = System.getenv("rateLimit"); appName = System.getenv("rateLimit");
System.out.println("rateLimit=" + appName);
return (appName != null) ? Boolean.valueOf(appName) : false; return (appName != null) ? Boolean.valueOf(appName) : false;
} }

View File

@ -16,33 +16,35 @@ import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class MetricsFilter extends OncePerRequestFilter { public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
@Autowired @Autowired
public MetricsFilter(MeterRegistry meterRegistry) { public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
} }
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
//System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources
if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) {
Counter counter = Counter.builder("http.requests")
.tag("uri", uri)
.tag("method", request.getMethod())
.register(meterRegistry);
counter.increment(); // System.out.println("uri="+uri + ", method=" + request.getMethod() );
//System.out.println("Counted"); // Ignore static resources
} if (!(uri.startsWith("/js") || uri.startsWith("/v1/api-docs") || uri.endsWith("robots.txt")
|| uri.startsWith("/images") || uri.startsWith("/images")|| uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".map")
|| uri.endsWith(".svg") || uri.endsWith(".js") || uri.contains("swagger")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/site.webmanifest") || uri.startsWith("/fonts") || uri.startsWith("/pdfjs") )) {
Counter counter = Counter.builder("http.requests").tag("uri", uri).tag("method", request.getMethod())
.register(meterRegistry);
filterChain.doFilter(request, response); counter.increment();
} // System.out.println("Counted");
}
filterChain.doFilter(request, response);
}
} }

View File

@ -1,27 +1,42 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Autowired
ApplicationProperties applicationProperties;
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
version = "1.0.0"; // default version if all else fails
version = "1.0.0"; // default version if all else fails }
} SecurityScheme apiKeyScheme = new SecurityScheme().type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER)
.name("X-API-KEY");
if (!applicationProperties.getSecurity().getEnableLogin()) {
return new OpenAPI().components(new Components())
.info(new Info().title("Stirling PDF API").version(version).description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else {
return new OpenAPI().components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
.info(new Info().title("Stirling PDF API").version(version).description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey"));
}
return new OpenAPI().components(new Components()).info(
new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} }
}
}

View File

@ -2,27 +2,46 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private final LoginAttemptService loginAttemptService;
@Autowired
public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override @Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
String ip = request.getRemoteAddr(); String ip = request.getRemoteAddr();
logger.error("Failed login attempt from IP: " + ip); logger.error("Failed login attempt from IP: " + ip);
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials"); String username = request.getParameter("username");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) { if(loginAttemptService.loginAttemptCheck(username)) {
setDefaultFailureUrl("/login?error=locked"); setDefaultFailureUrl("/login?error=locked");
} else {
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
} else if (exception.getClass().isAssignableFrom(LockedException.class)) {
setDefaultFailureUrl("/login?error=locked");
}
} }
super.onAuthenticationFailure(request, response, exception); super.onAuthenticationFailure(request, response, exception);
} }
} }

View File

@ -0,0 +1,44 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private LoginAttemptService loginAttemptService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
String username = request.getParameter("username");
loginAttemptService.loginSucceeded(username);
// Get the saved request
HttpSession session = request.getSession(false);
SavedRequest savedRequest = session != null ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") : null;
if (savedRequest != null && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
// Redirect to the root URL (considering context path)
getRedirectStrategy().sendRedirect(request, response, "/");
}
//super.onAuthenticationSuccess(request, response, authentication);
}
}

View File

@ -5,6 +5,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
@ -22,12 +23,18 @@ public class CustomUserDetailsService implements UserDetailsService {
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@Autowired
private LoginAttemptService loginAttemptService;
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) {
throw new LockedException("Your account has been locked due to too many failed login attempts.");
}
return new org.springframework.security.core.userdetails.User( return new org.springframework.security.core.userdetails.User(
user.getUsername(), user.getUsername(),
user.getPassword(), user.getPassword(),

View File

@ -15,6 +15,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.utils.RequestUriUtils;
@Component @Component
public class FirstLoginFilter extends OncePerRequestFilter { public class FirstLoginFilter extends OncePerRequestFilter {
@ -28,11 +29,7 @@ public class FirstLoginFilter extends OncePerRequestFilter {
String method = request.getMethod(); String method = request.getMethod();
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
// Check if the request is for static resources // Check if the request is for static resources
boolean isStaticResource = requestURI.startsWith("/css/") boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
|| requestURI.startsWith("/js/")
|| requestURI.startsWith("/images/")
|| requestURI.startsWith("/public/")
|| requestURI.endsWith(".svg");
// If it's a static resource, just continue the filter chain and skip the logic below // If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) { if (isStaticResource) {

View File

@ -0,0 +1,65 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.utils.RequestUriUtils;
public class IPRateLimitingFilter implements Filter {
private final ConcurrentHashMap<String, AtomicInteger> requestCounts = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, AtomicInteger> getCounts = new ConcurrentHashMap<>();
private final int maxRequests;
private final int maxGetRequests;
public IPRateLimitingFilter(int maxRequests, int maxGetRequests) {
this.maxRequests = maxRequests;
this.maxGetRequests = maxGetRequests;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String method = httpRequest.getMethod();
String requestURI = httpRequest.getRequestURI();
// Check if the request is for static resources
boolean isStaticResource = RequestUriUtils.isStaticResource(requestURI);
// If it's a static resource, just continue the filter chain and skip the logic below
if (isStaticResource) {
chain.doFilter(request, response);
return;
}
String clientIp = request.getRemoteAddr();
requestCounts.computeIfAbsent(clientIp, k -> new AtomicInteger(0));
if (!"GET".equalsIgnoreCase(method)) {
if (requestCounts.get(clientIp).incrementAndGet() > maxRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("Rate limit exceeded");
return;
}
} else {
if (requestCounts.get(clientIp).incrementAndGet() > maxGetRequests) {
// Handle limit exceeded (e.g., send error response)
response.getWriter().write("GET Rate limit exceeded");
return;
}
}
}
chain.doFilter(request, response);
}
public void resetRequestCounts() {
requestCounts.clear();
getCounts.clear();
}
}

View File

@ -37,8 +37,10 @@ public class InitialSecuritySetup {
initialPassword = "stirling"; initialPassword = "stirling";
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
} }
}
if(!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser(Role.INTERNAL_API_USER.getRoleId(), UUID.randomUUID().toString(), Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
} }
} }

View File

@ -0,0 +1,56 @@
package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AttemptCounter;
@Service
public class LoginAttemptService {
@Autowired
ApplicationProperties applicationProperties;
private int MAX_ATTEMPTS;
private long ATTEMPT_INCREMENT_TIME;
@PostConstruct
public void init() {
MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = TimeUnit.MINUTES.toMillis(applicationProperties.getSecurity().getLoginResetTimeMinutes());
}
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache = new ConcurrentHashMap<>();
public void loginSucceeded(String key) {
attemptsCache.remove(key);
}
public boolean loginAttemptCheck(String key) {
attemptsCache.compute(key, (k, attemptCounter) -> {
if (attemptCounter == null || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
return new AttemptCounter();
} else {
attemptCounter.increment();
return attemptCounter;
}
});
return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
}
public boolean isBlocked(String key) {
AttemptCounter attemptCounter = attemptsCache.get(key);
if (attemptCounter != null) {
return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
}
return false;
}
}

View File

@ -0,0 +1,18 @@
package stirling.software.SPDF.config.security;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class RateLimitResetScheduler {
private final IPRateLimitingFilter rateLimitingFilter;
public RateLimitResetScheduler(IPRateLimitingFilter rateLimitingFilter) {
this.rateLimitingFilter = rateLimitingFilter;
}
@Scheduled(cron = "0 0 0 * * MON") // At 00:00 every Monday TODO: configurable
public void resetRateLimit() {
rateLimitingFilter.resetRequestCounts();
}
}

View File

@ -6,7 +6,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
@ -15,12 +15,13 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
@Configuration @Configuration
@EnableWebSecurity() @EnableWebSecurity()
@EnableGlobalMethodSecurity(prePostEnabled = true) @EnableMethodSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
@Autowired @Autowired
@ -40,6 +41,11 @@ public class SecurityConfiguration {
@Autowired @Autowired
private UserAuthenticationFilter userAuthenticationFilter; private UserAuthenticationFilter userAuthenticationFilter;
@Autowired
private LoginAttemptService loginAttemptService;
@Autowired @Autowired
private FirstLoginFilter firstLoginFilter; private FirstLoginFilter firstLoginFilter;
@ -51,14 +57,18 @@ public class SecurityConfiguration {
if(loginEnabledValue) { if(loginEnabledValue) {
http.csrf(csrf -> csrf.disable()); http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http http
.formLogin(formLogin -> formLogin .formLogin(formLogin -> formLogin
.loginPage("/login") .loginPage("/login")
.successHandler(new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/") .defaultSuccessUrl("/")
.failureHandler(new CustomAuthenticationFailureHandler()) .failureHandler(new CustomAuthenticationFailureHandler(loginAttemptService))
.permitAll() .permitAll()
) ).requestCache(requestCache -> requestCache
.requestCache(new NullRequestCache())
)
.logout(logout -> logout .logout(logout -> logout
.logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.logoutSuccessUrl("/login?logout=true") .logoutSuccessUrl("/login?logout=true")
@ -70,8 +80,19 @@ public class SecurityConfiguration {
.tokenValiditySeconds(1209600) // 2 weeks .tokenValiditySeconds(1209600) // 2 weeks
) )
.authorizeHttpRequests(authz -> authz .authorizeHttpRequests(authz -> authz
.requestMatchers(req -> req.getRequestURI().startsWith("/login") || req.getRequestURI().endsWith(".svg") || req.getRequestURI().startsWith("/register") || req.getRequestURI().startsWith("/error") || req.getRequestURI().startsWith("/images/") || req.getRequestURI().startsWith("/public/") || req.getRequestURI().startsWith("/css/") || req.getRequestURI().startsWith("/js/")) .requestMatchers(req -> {
.permitAll() String uri = req.getRequestURI();
String contextPath = req.getContextPath();
// Remove the context path from the URI
String trimmedUri = uri.startsWith(contextPath) ? uri.substring(contextPath.length()) : uri;
return trimmedUri.startsWith("/login") || trimmedUri.endsWith(".svg") ||
trimmedUri.startsWith("/register") || trimmedUri.startsWith("/error") ||
trimmedUri.startsWith("/images/") || trimmedUri.startsWith("/public/") ||
trimmedUri.startsWith("/css/") || trimmedUri.startsWith("/js/");
}
).permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.userDetailsService(userDetailsService) .userDetailsService(userDetailsService)
@ -84,8 +105,17 @@ public class SecurityConfiguration {
} }
return http.build(); return http.build();
} }
@Bean
public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
}
@Bean @Bean
public DaoAuthenticationProvider authenticationProvider() { public DaoAuthenticationProvider authenticationProvider() {

View File

@ -74,15 +74,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
// If we still don't have any authentication, deny the request // If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod(); String method = request.getMethod();
if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) { String contextPath = request.getContextPath();
response.sendRedirect("/login"); // redirect to the login page
if ("GET".equalsIgnoreCase(method) && ! (contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page
return; return;
} else { } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected"); response.getWriter().write("Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return; return;
} }
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
@ -90,15 +92,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
String contextPath = request.getContextPath();
String[] permitAllPatterns = { String[] permitAllPatterns = {
"/login", contextPath + "/login",
"/register", contextPath + "/register",
"/error", contextPath + "/error",
"/images/", contextPath + "/images/",
"/public/", contextPath + "/public/",
"/css/", contextPath + "/css/",
"/js/" contextPath + "/js/",
contextPath + "/pdfjs/",
contextPath + "/site.webmanifest"
}; };
for (String pattern : permitAllPatterns) { for (String pattern : permitAllPatterns) {

View File

@ -16,11 +16,13 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class UserService { public class UserService implements UserServiceInterface{
@Autowired @Autowired
private UserRepository userRepository; private UserRepository userRepository;
@ -136,6 +138,11 @@ public class UserService {
public void deleteUser(String username) { public void deleteUser(String username) {
Optional<User> userOpt = userRepository.findByUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
for (Authority authority : userOpt.get().getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return;
}
}
userRepository.delete(userOpt.get()); userRepository.delete(userOpt.get());
} }
} }

View File

@ -3,8 +3,9 @@ import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ArrayList;
import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@ -25,7 +26,7 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class PdfOverlayController { public class PdfOverlayController {
@PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data") @PostMapping(value = "/overlay-pdfs", consumes = "multipart/form-data")
@Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO") @Operation(summary = "Overlay PDF files in various modes", description = "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException {
MultipartFile baseFile = request.getFileInput(); MultipartFile baseFile = request.getFileInput();
@ -33,44 +34,53 @@ public class PdfOverlayController {
MultipartFile[] overlayFiles = request.getOverlayFiles(); MultipartFile[] overlayFiles = request.getOverlayFiles();
File[] overlayPdfFiles = new File[overlayFiles.length]; File[] overlayPdfFiles = new File[overlayFiles.length];
try{ List<File> tempFiles = new ArrayList<>(); // List to keep track of temporary files
for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]); try {
} for (int i = 0; i < overlayFiles.length; i++) {
overlayPdfFiles[i] = GeneralUtils.multipartToFile(overlayFiles[i]);
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay" }
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
String mode = request.getOverlayMode(); // "SequentialOverlay", "InterleavedOverlay", "FixedRepeatOverlay"
try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts); try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
Overlay overlay = new Overlay()) {
overlay.setInputPDF(basePdf); Map<Integer, String> overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts, tempFiles);
if(overlayPos == 0) {
overlay.setOverlayPosition(Overlay.Position.FOREGROUND); overlay.setInputPDF(basePdf);
} else { if (overlayPos == 0) {
overlay.setOverlayPosition(Overlay.Position.BACKGROUND); overlay.setOverlayPosition(Overlay.Position.FOREGROUND);
} } else {
overlay.setOverlayPosition(Overlay.Position.BACKGROUND);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); }
overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray();
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf
}
return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF);
}
} finally { } finally {
for (File overlayPdfFile : overlayPdfFiles) { for (File overlayPdfFile : overlayPdfFiles) {
if (overlayPdfFile != null) overlayPdfFile.delete(); if (overlayPdfFile != null) {
overlayPdfFile.delete();
}
}
for (File tempFile : tempFiles) { // Delete temporary files
if (tempFile != null) {
tempFile.delete();
}
} }
} }
} }
private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts) throws IOException { private Map<Integer, String> prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts, List<File> tempFiles) throws IOException {
Map<Integer, String> overlayGuide = new HashMap<>(); Map<Integer, String> overlayGuide = new HashMap<>();
switch (mode) { switch (mode) {
case "SequentialOverlay": case "SequentialOverlay":
sequentialOverlay(overlayGuide, overlayFiles, basePageCount); sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles);
break; break;
case "InterleavedOverlay": case "InterleavedOverlay":
interleavedOverlay(overlayGuide, overlayFiles, basePageCount); interleavedOverlay(overlayGuide, overlayFiles, basePageCount);
@ -84,42 +94,80 @@ public class PdfOverlayController {
return overlayGuide; return overlayGuide;
} }
private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { private void sequentialOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount, List<File> tempFiles) throws IOException {
if (overlayFiles.length != 1 || basePageCount != PDDocument.load(overlayFiles[0]).getNumberOfPages()) { int overlayFileIndex = 0;
throw new IllegalArgumentException("Overlay file count and base page count must match for sequential overlay."); int pageCountInCurrentOverlay = 0;
}
File overlayFile = overlayFiles[0]; for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) { if (pageCountInCurrentOverlay == 0 || pageCountInCurrentOverlay >= getNumberOfPages(overlayFiles[overlayFileIndex])) {
for (int i = 1; i <= overlayPdf.getNumberOfPages(); i++) { pageCountInCurrentOverlay = 0;
if (i > basePageCount) break; overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
overlayGuide.put(i, overlayFile.getAbsolutePath()); }
try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) {
PDDocument singlePageDocument = new PDDocument();
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
File tempFile = File.createTempFile("overlay-page-", ".pdf");
singlePageDocument.save(tempFile);
singlePageDocument.close();
overlayGuide.put(basePageIndex, tempFile.getAbsolutePath());
tempFiles.add(tempFile); // Keep track of the temporary file for cleanup
}
pageCountInCurrentOverlay++;
}
}
private int getNumberOfPages(File file) throws IOException {
try (PDDocument doc = PDDocument.load(file)) {
return doc.getNumberOfPages();
}
}
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) {
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
// Load the overlay document to check its page count
try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages();
if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) {
overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath());
}
} }
} }
} }
private void interleavedOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int basePageCount) throws IOException {
for (int i = 0; i < basePageCount; i++) {
File overlayFile = overlayFiles[i % overlayFiles.length];
overlayGuide.put(i + 1, overlayFile.getAbsolutePath());
}
}
private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { private void fixedRepeatOverlay(Map<Integer, String> overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException {
if (overlayFiles.length != counts.length) { if (overlayFiles.length != counts.length) {
throw new IllegalArgumentException("Counts array length must match the number of overlay files"); throw new IllegalArgumentException("Counts array length must match the number of overlay files");
} }
int currentPage = 1; int currentPage = 1;
for (int i = 0; i < overlayFiles.length; i++) { for (int i = 0; i < overlayFiles.length; i++) {
File overlayFile = overlayFiles[i]; File overlayFile = overlayFiles[i];
int repeatCount = counts[i]; int repeatCount = counts[i];
for (int j = 0; j < repeatCount; j++) {
if (currentPage > basePageCount) break; // Load the overlay document to check its page count
overlayGuide.put(currentPage++, overlayFile.getAbsolutePath()); try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages();
for (int j = 0; j < repeatCount; j++) {
for (int page = 0; page < overlayPageCount; page++) {
if (currentPage > basePageCount) break;
overlayGuide.put(currentPage++, overlayFile.getAbsolutePath());
}
}
} }
} }
} }
} }
// Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere. // Additional classes like OverlayPdfsRequest, WebResponseUtils, etc. are assumed to be defined elsewhere.

View File

@ -29,12 +29,12 @@ import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
@PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data") @PostMapping(value = "/split-pdf-by-sections", consumes = "multipart/form-data")
@Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input: PDF, Split Parameters. Output: ZIP containing split documents.") @Operation(summary = "Split PDF pages into smaller sections", description = "Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();

View File

@ -26,13 +26,13 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@RequestMapping("/api/v1/general") @RequestMapping("/api/v1/general")
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
@PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data") @PostMapping(value = "/split-by-size-or-count", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n" @Operation(summary = "Auto split PDF pages into separate documents based on size or count", description = "split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP Type:SIMO") + " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<ByteArrayOutputStream>();

View File

@ -23,6 +23,7 @@ import org.springframework.web.servlet.view.RedirectView;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.config.security.UserService; import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@Controller @Controller
@ -32,6 +33,7 @@ public class UserController {
@Autowired @Autowired
private UserService userService; private UserService userService;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@RequestParam String username, @RequestParam String password, Model model) { public String register(@RequestParam String username, @RequestParam String password, Model model) {
if(userService.usernameExists(username)) { if(userService.usernameExists(username)) {
@ -43,6 +45,7 @@ public class UserController {
return "redirect:/login?registered=true"; return "redirect:/login?registered=true";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username-and-password") @PostMapping("/change-username-and-password")
public RedirectView changeUsernameAndPassword(Principal principal, public RedirectView changeUsernameAndPassword(Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@ -85,7 +88,7 @@ public class UserController {
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-username") @PostMapping("/change-username")
public RedirectView changeUsername(Principal principal, public RedirectView changeUsername(Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@ -122,7 +125,8 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/change-password") @PostMapping("/change-password")
public RedirectView changePassword(Principal principal, public RedirectView changePassword(Principal principal,
@RequestParam String currentPassword, @RequestParam String currentPassword,
@ -154,7 +158,7 @@ public class UserController {
return new RedirectView("/login?messageType=credsUpdated"); return new RedirectView("/login?messageType=credsUpdated");
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/updateUserSettings") @PostMapping("/updateUserSettings")
public String updateUserSettings(HttpServletRequest request, Principal principal) { public String updateUserSettings(HttpServletRequest request, Principal principal) {
Map<String, String[]> paramMap = request.getParameterMap(); Map<String, String[]> paramMap = request.getParameterMap();
@ -182,6 +186,18 @@ public class UserController {
if(userService.usernameExists(username)) { if(userService.usernameExists(username)) {
return new RedirectView("/addUsers?messageType=usernameExists"); return new RedirectView("/addUsers?messageType=usernameExists");
} }
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/addUsers?messageType=invalidRole");
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/addUsers?messageType=invalidRole");
}
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
return new RedirectView("/addUsers"); // Redirect to account page after adding the user return new RedirectView("/addUsers"); // Redirect to account page after adding the user
} }
@ -203,6 +219,7 @@ public class UserController {
return "redirect:/addUsers"; return "redirect:/addUsers";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/get-api-key") @PostMapping("/get-api-key")
public ResponseEntity<String> getApiKey(Principal principal) { public ResponseEntity<String> getApiKey(Principal principal) {
if (principal == null) { if (principal == null) {
@ -216,6 +233,7 @@ public class UserController {
return ResponseEntity.ok(apiKey); return ResponseEntity.ok(apiKey);
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/update-api-key") @PostMapping("/update-api-key")
public ResponseEntity<String> updateApiKey(Principal principal) { public ResponseEntity<String> updateApiKey(Principal principal) {
if (principal == null) { if (principal == null) {

View File

@ -1,130 +0,0 @@
package stirling.software.SPDF.controller.api.converters;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
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.RestController;
import org.springframework.web.multipart.MultipartFile;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/convert")
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertEpubToPdf {
//TODO
@PostMapping(consumes = "multipart/form-data", value = "/epub-to-single-pdf")
@Hidden
@Operation(
summary = "Convert an EPUB file to a single PDF",
description = "This endpoint takes an EPUB file input and converts it to a single PDF."
)
public ResponseEntity<byte[]> epubToSinglePdf(
@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();
if (fileInput == null) {
throw new IllegalArgumentException("Please provide an EPUB file for conversion.");
}
String originalFilename = fileInput.getOriginalFilename();
if (originalFilename == null || !originalFilename.endsWith(".epub")) {
throw new IllegalArgumentException("File must be in .epub format.");
}
Map<String, byte[]> epubContents = extractEpubContent(fileInput);
List<String> htmlFilesOrder = getHtmlFilesOrderFromOpf(epubContents);
List<byte[]> individualPdfs = new ArrayList<>();
for (String htmlFile : htmlFilesOrder) {
byte[] htmlContent = epubContents.get(htmlFile);
byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent, htmlFile.replace(".html", ".pdf"));
individualPdfs.add(pdfBytes);
}
// Pseudo-code to merge individual PDFs into one.
byte[] mergedPdfBytes = mergeMultiplePdfsIntoOne(individualPdfs);
return WebResponseUtils.bytesToWebResponse(mergedPdfBytes, originalFilename.replace(".epub", ".pdf"));
}
// Assuming a pseudo-code function that merges multiple PDFs into one.
private byte[] mergeMultiplePdfsIntoOne(List<byte[]> individualPdfs) {
// You can use a library such as PDFBox to perform the merging here.
// Return the byte[] of the merged PDF.
return null;
}
private Map<String, byte[]> extractEpubContent(MultipartFile fileInput) throws IOException {
Map<String, byte[]> contentMap = new HashMap<>();
try (ZipInputStream zis = new ZipInputStream(fileInput.getInputStream())) {
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int read = 0;
while ((read = zis.read(buffer)) != -1) {
baos.write(buffer, 0, read);
}
contentMap.put(zipEntry.getName(), baos.toByteArray());
zipEntry = zis.getNextEntry();
}
}
return contentMap;
}
private List<String> getHtmlFilesOrderFromOpf(Map<String, byte[]> epubContents) throws Exception {
String opfContent = new String(epubContents.get("OEBPS/content.opf")); // Adjusting for given path
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
InputSource is = new InputSource(new StringReader(opfContent));
Document doc = dBuilder.parse(is);
NodeList itemRefs = doc.getElementsByTagName("itemref");
List<String> htmlFilesOrder = new ArrayList<>();
for (int i = 0; i < itemRefs.getLength(); i++) {
Element itemRef = (Element) itemRefs.item(i);
String idref = itemRef.getAttribute("idref");
NodeList items = doc.getElementsByTagName("item");
for (int j = 0; j < items.getLength(); j++) {
Element item = (Element) items.item(j);
if (idref.equals(item.getAttribute("id"))) {
htmlFilesOrder.add(item.getAttribute("href")); // Fetching the actual href
break;
}
}
}
return htmlFilesOrder;
}
}

View File

@ -28,7 +28,7 @@ public class ConvertWebsiteToPDF {
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf") @PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation( @Operation(
summary = "Convert a URL to a PDF", summary = "Convert a URL to a PDF",
description = "This endpoint fetches content from a URL and converts it to a PDF format." description = "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO"
) )
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException {
String URL = request.getUrlInput(); String URL = request.getUrlInput();

View File

@ -28,12 +28,12 @@ import stirling.software.SPDF.model.api.extract.PDFFilePage;
@RestController @RestController
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
@Tag(name = "General", description = "General APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ExtractController { public class ExtractController {
private static final Logger logger = LoggerFactory.getLogger(CropController.class); private static final Logger logger = LoggerFactory.getLogger(CropController.class);
@PostMapping(value = "/pdf-to-csv", consumes = "multipart/form-data") @PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") @Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form)
throws Exception { throws Exception {

View File

@ -43,7 +43,7 @@ public class AutoSplitPdfController {
private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF";
@PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data") @PostMapping(value = "/auto-split-pdf", consumes = "multipart/form-data")
@Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP Type:SISO") @Operation(summary = "Auto split PDF pages into separate documents", description = "This endpoint accepts a PDF file, scans each page for a specific QR code, and splits the document at the QR code boundaries. The output is a zip file containing each separate PDF document. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
boolean duplexMode = request.isDuplexMode(); boolean duplexMode = request.isDuplexMode();

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -25,6 +26,7 @@ public class ShowJavascript {
private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class);
@PostMapping(consumes = "multipart/form-data", value = "/show-javascript") @PostMapping(consumes = "multipart/form-data", value = "/show-javascript")
@Operation(summary = "Grabs all JS from a PDF and returns a single JS file with all code", description = "desc. Input:PDF Output:JS Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception { public ResponseEntity<byte[]> extractHeader(@ModelAttribute PDFFile request) throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
String script = ""; String script = "";

View File

@ -0,0 +1,123 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.ApiEndpoint;
import stirling.software.SPDF.model.Role;
import org.slf4j.Logger;
@Service
public class ApiDocService {
private final Map<String, ApiEndpoint> apiDocumentation = new HashMap<>();
private static final Logger logger = LoggerFactory.getLogger(ApiDocService.class);
@Autowired
private ServletContext servletContext;
private String getApiDocsUrl() {
String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getPort();
return "http://localhost:"+ port + contextPath + "/v1/api-docs";
}
@Autowired(required=false)
private UserServiceInterface userService;
private String getApiKeyForUser() {
if(userService == null)
return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
JsonNode apiDocsJsonRootNode;
//@EventListener(ApplicationReadyEvent.class)
private synchronized void loadApiDocumentation() {
String apiDocsJson = "";
try {
HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser();
if (!apiKey.isEmpty()) {
headers.set("X-API-KEY", apiKey);
}
HttpEntity<String> entity = new HttpEntity<>(headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(getApiDocsUrl(), HttpMethod.GET, entity, String.class);
apiDocsJson = response.getBody();
ObjectMapper mapper = new ObjectMapper();
apiDocsJsonRootNode = mapper.readTree(apiDocsJson);
JsonNode paths = apiDocsJsonRootNode.path("paths");
paths.fields().forEachRemaining(entry -> {
String path = entry.getKey();
JsonNode pathNode = entry.getValue();
if (pathNode.has("post")) {
JsonNode postNode = pathNode.get("post");
ApiEndpoint endpoint = new ApiEndpoint(path, postNode);
apiDocumentation.put(path, endpoint);
}
});
} catch (Exception e) {
// Handle exceptions
logger.error("Error grabbing swagger doc, body result {}", apiDocsJson);
}
}
public boolean isValidOperation(String operationName, Map<String, Object> parameters) {
if(apiDocumentation.size() == 0) {
loadApiDocumentation();
}
if (!apiDocumentation.containsKey(operationName)) {
return false;
}
ApiEndpoint endpoint = apiDocumentation.get(operationName);
return endpoint.areParametersValid(parameters);
}
public boolean isMultiInput(String operationName) {
if(apiDocsJsonRootNode == null || apiDocumentation.size() == 0) {
loadApiDocumentation();
}
if (!apiDocumentation.containsKey(operationName)) {
return false;
}
ApiEndpoint endpoint = apiDocumentation.get(operationName);
String description = endpoint.getDescription();
Pattern pattern = Pattern.compile("Type:(\\w+)");
Matcher matcher = pattern.matcher(description);
if (matcher.find()) {
String type = matcher.group(1);
return type.startsWith("MI");
}
return false;
}
}
// Model class for API Endpoint

View File

@ -1,56 +1,31 @@
package stirling.software.SPDF.controller.api.pipeline; package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.model.api.HandleDataRequest;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -60,373 +35,39 @@ import stirling.software.SPDF.utils.WebResponseUtils;
public class PipelineController { public class PipelineController {
private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); private static final Logger logger = LoggerFactory.getLogger(PipelineController.class);
@Autowired
private ObjectMapper objectMapper;
final String jsonFileName = "pipelineConfig.json";
final String watchedFoldersDir = "./pipeline/watchedFolders/"; final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/"; final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired
PipelineProcessor processor;
@Scheduled(fixedRate = 25000)
public void scanFolders() {
logger.info("Scanning folders...");
Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e);
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
logger.error("Error handling directory: {}", t, e);
}
});
} catch (Exception e) {
logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
@Autowired @Autowired
ApplicationProperties applicationProperties; ApplicationProperties applicationProperties;
@Autowired
private void handleDirectory(Path dir) throws Exception { private ObjectMapper objectMapper;
logger.info("Handling directory: {}", dir);
Path jsonFile = dir.resolve(jsonFileName);
Path processingDir = dir.resolve("processing"); // Directory to move files during processing
if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir);
logger.info("Created processing directory: {}", processingDir);
}
if (Files.exists(jsonFile)) {
// Read JSON file
String jsonString;
try {
jsonString = new String(Files.readAllBytes(jsonFile));
logger.info("Read JSON file: {}", jsonFile);
} catch (IOException e) {
logger.error("Error reading JSON file: {}", jsonFile, e);
return;
}
// Decode JSON to PipelineConfig
PipelineConfig config;
try {
config = objectMapper.readValue(jsonString, PipelineConfig.class);
// Assuming your PipelineConfig class has getters for all necessary fields, you
// can perform checks here
if (config.getOperations() == null || config.getOutputDir() == null || config.getName() == null) {
throw new IOException("Invalid JSON format");
}
} catch (IOException e) {
logger.error("Error parsing PipelineConfig: {}", jsonString, e);
return;
}
// For each operation in the pipeline
for (PipelineOperation operation : config.getOperations()) {
// Collect all files based on fileInput
File[] files;
String fileInput = (String) operation.getParameters().get("fileInput");
if ("automated".equals(fileInput)) {
// If fileInput is "automated", process all files in the directory
try (Stream<Path> paths = Files.list(dir)) {
files = paths
.filter(path -> !Files.isDirectory(path)) // exclude directories
.filter(path -> !path.equals(jsonFile)) // exclude jsonFile
.map(Path::toFile)
.toArray(File[]::new);
} catch (IOException e) {
e.printStackTrace();
return;
}
} else {
// If fileInput contains a path, process only this file
files = new File[] { new File(fileInput) };
}
// Prepare the files for processing
List<File> filesToProcess = new ArrayList<>();
for (File file : files) {
logger.info(file.getName());
logger.info("{} to {}",file.toPath(), processingDir.resolve(file.getName()));
Files.move(file.toPath(), processingDir.resolve(file.getName()));
filesToProcess.add(processingDir.resolve(file.getName()).toFile());
}
// Process the files
try {
List<Resource> resources = handleFiles(filesToProcess.toArray(new File[0]), jsonString);
if(resources == null) {
return;
}
// Move resultant files and rename them as per config in JSON file
for (Resource resource : resources) {
String resourceName = resource.getFilename();
String baseName = resourceName.substring(0, resourceName.lastIndexOf("."));
String extension = resourceName.substring(resourceName.lastIndexOf(".")+1);
String outputFileName = config.getOutputPattern().replace("{filename}", baseName);
outputFileName = outputFileName.replace("{pipelineName}", config.getName());
DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
outputFileName = outputFileName.replace("{date}", LocalDate.now().format(dateFormatter));
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmmss");
outputFileName = outputFileName.replace("{time}", LocalTime.now().format(timeFormatter));
outputFileName += "." + extension;
// {filename} {folder} {date} {tmime} {pipeline}
String outputDir = config.getOutputDir();
String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder();
if (outputFolder == null || outputFolder.isEmpty()) {
// If the environment variable is not set, use the default value
outputFolder = finishedFoldersDir;
}
logger.info("outputDir 0={}", outputDir);
// Replace the placeholders in the outputDir string
outputDir = outputDir.replace("{outputFolder}", outputFolder);
outputDir = outputDir.replace("{folderName}", dir.toString());
logger.info("outputDir 1={}", outputDir);
outputDir = outputDir.replace("\\watchedFolders", "");
outputDir = outputDir.replace("//watchedFolders", "");
outputDir = outputDir.replace("\\\\watchedFolders", "");
outputDir = outputDir.replace("/watchedFolders", "");
Path outputPath;
logger.info("outputDir 2={}", outputDir);
if (Paths.get(outputDir).isAbsolute()) {
// If it's an absolute path, use it directly
outputPath = Paths.get(outputDir);
} else {
// If it's a relative path, make it relative to the current working directory
outputPath = Paths.get(".", outputDir);
}
logger.info("outputPath={}", outputPath);
if (!Files.exists(outputPath)) {
try {
Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath);
} catch (IOException e) {
logger.error("Error creating directory: {}", outputPath, e);
return;
}
}
logger.info("outputPath {}", outputPath);
logger.info("outputPath.resolve(outputFileName).toString() {}", outputPath.resolve(outputFileName).toString());
File newFile = new File(outputPath.resolve(outputFileName).toString());
OutputStream os = new FileOutputStream(newFile);
os.write(((ByteArrayResource)resource).getByteArray());
os.close();
logger.info("made {}", outputPath.resolve(outputFileName));
}
// If successful, delete the original files
for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName()));
}
} catch (Exception e) {
// If an error occurs, move the original files back
for (File file : filesToProcess) {
Files.move(processingDir.resolve(file.getName()), file.toPath());
}
throw e;
}
}
}
}
List<Resource> processFiles(List<Resource> outputFiles, String jsonString) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
logger.info("Running pipelineNode: {}", pipelineNode);
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
for (JsonNode operationNode : pipelineNode) {
String operation = operationNode.get("operation").asText();
logger.info("Running operation: {}", operation);
JsonNode parametersNode = operationNode.get("parameters");
String inputFileExtension = "";
if (operationNode.has("inputFileType")) {
inputFileExtension = operationNode.get("inputFileType").asText();
} else {
inputFileExtension = ".pdf";
}
List<Resource> newOutputFiles = new ArrayList<>();
boolean hasInputFileType = false;
for (Resource file : outputFiles) {
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
Iterator<Map.Entry<String, JsonNode>> parameters = parametersNode.fields();
while (parameters.hasNext()) {
Map.Entry<String, JsonNode> parameter = parameters.next();
body.add(parameter.getKey(), parameter.getValue().asText());
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
RestTemplate restTemplate = new RestTemplate();
String url = "http://localhost:8080/" + operation;
ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
// If the operation is filter and the response body is null or empty, skip this file
if (operation.startsWith("filter-") && (response.getBody() == null || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation);
continue;
}
if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody());
hasErrors = true;
continue;
}
// Define filename
String filename;
if ("auto-rename".equals(operation)) {
// If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp.
// Modify as per your needs.
filename = "file_" + System.currentTimeMillis();
} else {
// Otherwise, keep the original filename.
filename = file.getFilename();
}
// Check if the response body is a zip file
if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return filename;
}
};
newOutputFiles.add(outputResource);
}
}
if (!hasInputFileType) {
logPrintStream.println(
"No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true;
}
outputFiles = newOutputFiles;
}
logPrintStream.close();
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
List<Resource> handleFiles(File[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) {
logger.info("No files");
return null;
}
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (File file : files) {
Path path = Paths.get(file.getAbsolutePath());
System.out.println("Reading file: " + path); // debug statement
if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
} else {
System.out.println("File not found: " + path); // debug statement
}
}
logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString);
}
List<Resource> handleFiles(MultipartFile[] files, String jsonString) throws Exception {
if(files == null || files.length == 0) {
logger.info("No files");
return null;
}
logger.info("Handling files: {} files, with JSON string of length: {}", files.length, jsonString.length());
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(jsonString);
JsonNode pipelineNode = jsonNode.get("pipeline");
boolean hasErrors = false;
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return processFiles(outputFiles, jsonString);
}
@PostMapping("/handleData") @PostMapping("/handleData")
public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) { public ResponseEntity<byte[]> handleData(@ModelAttribute HandleDataRequest request) throws JsonMappingException, JsonProcessingException {
MultipartFile[] files = request.getFileInputs(); if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
String jsonString = request.getJsonString(); return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
MultipartFile[] files = request.getFileInput();
String jsonString = request.getJson();
if (files == null) {
return null;
}
PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class);
logger.info("Received POST request to /handleData with {} files", files.length); logger.info("Received POST request to /handleData with {} files", files.length);
try { try {
List<Resource> outputFiles = handleFiles(files, jsonString); List<Resource> inputFiles = processor.generateInputFiles(files);
if(inputFiles == null || inputFiles.size() == 0) {
return null;
}
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
if (outputFiles != null && outputFiles.size() == 1) { if (outputFiles != null && outputFiles.size() == 1) {
// If there is only one file, return it directly // If there is only one file, return it directly
Resource singleFile = outputFiles.get(0); Resource singleFile = outputFiles.get(0);
@ -473,52 +114,4 @@ public class PipelineController {
} }
} }
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
} }

View File

@ -0,0 +1,258 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
@Service
public class PipelineDirectoryProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineDirectoryProcessor.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ApiDocService apiDocService;
@Autowired
private ApplicationProperties applicationProperties;
final String watchedFoldersDir = "./pipeline/watchedFolders/";
final String finishedFoldersDir = "./pipeline/finishedFolders/";
@Autowired
PipelineProcessor processor;
@Scheduled(fixedRate = 60000)
public void scanFolders() {
if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) {
return;
}
Path watchedFolderPath = Paths.get(watchedFoldersDir);
if (!Files.exists(watchedFolderPath)) {
try {
Files.createDirectories(watchedFolderPath);
logger.info("Created directory: {}", watchedFolderPath);
} catch (IOException e) {
logger.error("Error creating directory: {}", watchedFolderPath, e);
return;
}
}
try (Stream<Path> paths = Files.walk(watchedFolderPath)) {
paths.filter(Files::isDirectory).forEach(t -> {
try {
if (!t.equals(watchedFolderPath) && !t.endsWith("processing")) {
handleDirectory(t);
}
} catch (Exception e) {
logger.error("Error handling directory: {}", t, e);
}
});
} catch (Exception e) {
logger.error("Error walking through directory: {}", watchedFolderPath, e);
}
}
public void handleDirectory(Path dir) throws IOException {
logger.info("Handling directory: {}", dir);
Path processingDir = createProcessingDirectory(dir);
Optional<Path> jsonFileOptional = findJsonFile(dir);
if (!jsonFileOptional.isPresent()) {
logger.warn("No .JSON settings file found. No processing will happen for dir {}.", dir);
return;
}
Path jsonFile = jsonFileOptional.get();
PipelineConfig config = readAndParseJson(jsonFile);
processPipelineOperations(dir, processingDir, jsonFile, config);
}
private Path createProcessingDirectory(Path dir) throws IOException {
Path processingDir = dir.resolve("processing");
if (!Files.exists(processingDir)) {
Files.createDirectory(processingDir);
logger.info("Created processing directory: {}", processingDir);
}
return processingDir;
}
private Optional<Path> findJsonFile(Path dir) throws IOException {
try (Stream<Path> paths = Files.list(dir)) {
return paths.filter(file -> file.toString().endsWith(".json")).findFirst();
}
}
private PipelineConfig readAndParseJson(Path jsonFile) throws IOException {
String jsonString = new String(Files.readAllBytes(jsonFile), StandardCharsets.UTF_8);
logger.debug("Reading JSON file: {}", jsonFile);
return objectMapper.readValue(jsonString, PipelineConfig.class);
}
private void processPipelineOperations(Path dir, Path processingDir, Path jsonFile, PipelineConfig config) throws IOException {
for (PipelineOperation operation : config.getOperations()) {
validateOperation(operation);
File[] files = collectFilesForProcessing(dir, jsonFile, operation);
if(files == null || files.length == 0) {
logger.debug("No files detected for {} ", dir);
return;
}
List<File> filesToProcess = prepareFilesForProcessing(files, processingDir);
runPipelineAgainstFiles(filesToProcess, config, dir, processingDir);
}
}
private void validateOperation(PipelineOperation operation) throws IOException {
if (!apiDocService.isValidOperation(operation.getOperation(), operation.getParameters())) {
throw new IOException("Invalid operation: " + operation.getOperation());
}
}
private File[] collectFilesForProcessing(Path dir, Path jsonFile, PipelineOperation operation) throws IOException {
try (Stream<Path> paths = Files.list(dir)) {
if ("automated".equals(operation.getParameters().get("fileInput"))) {
return paths.filter(path -> !Files.isDirectory(path) && !path.equals(jsonFile))
.map(Path::toFile)
.toArray(File[]::new);
} else {
String fileInput = (String) operation.getParameters().get("fileInput");
return new File[]{new File(fileInput)};
}
}
}
private List<File> prepareFilesForProcessing(File[] files, Path processingDir) throws IOException {
List<File> filesToProcess = new ArrayList<>();
for (File file : files) {
Path targetPath = resolveUniqueFilePath(processingDir, file.getName());
Files.move(file.toPath(), targetPath);
filesToProcess.add(targetPath.toFile());
}
return filesToProcess;
}
private Path resolveUniqueFilePath(Path directory, String originalFileName) {
Path filePath = directory.resolve(originalFileName);
int counter = 1;
while (Files.exists(filePath)) {
String newName = appendSuffixToFileName(originalFileName, "(" + counter + ")");
filePath = directory.resolve(newName);
counter++;
}
return filePath;
}
private String appendSuffixToFileName(String originalFileName, String suffix) {
int dotIndex = originalFileName.lastIndexOf('.');
if (dotIndex == -1) {
return originalFileName + suffix;
} else {
return originalFileName.substring(0, dotIndex) + suffix + originalFileName.substring(dotIndex);
}
}
private void runPipelineAgainstFiles(List<File> filesToProcess, PipelineConfig config, Path dir, Path processingDir) throws IOException {
try {
List<Resource> inputFiles = processor.generateInputFiles(filesToProcess.toArray(new File[0]));
if(inputFiles == null || inputFiles.size() == 0) {
return;
}
List<Resource> outputFiles = processor.runPipelineAgainstFiles(inputFiles, config);
if (outputFiles == null) return;
moveAndRenameFiles(outputFiles, config, dir);
deleteOriginalFiles(filesToProcess, processingDir);
} catch (Exception e) {
logger.error("error during processing", e);
moveFilesBack(filesToProcess, processingDir);
}
}
private void moveAndRenameFiles(List<Resource> resources, PipelineConfig config, Path dir) throws IOException {
for (Resource resource : resources) {
String outputFileName = createOutputFileName(resource, config);
Path outputPath = determineOutputPath(config, dir);
if (!Files.exists(outputPath)) {
Files.createDirectories(outputPath);
logger.info("Created directory: {}", outputPath);
}
Path outputFile = outputPath.resolve(outputFileName);
try (OutputStream os = new FileOutputStream(outputFile.toFile())) {
os.write(((ByteArrayResource) resource).getByteArray());
}
logger.info("File moved and renamed to {}", outputFile);
}
}
private String createOutputFileName(Resource resource, PipelineConfig config) {
String resourceName = resource.getFilename();
String baseName = resourceName.substring(0, resourceName.lastIndexOf('.'));
String extension = resourceName.substring(resourceName.lastIndexOf('.') + 1);
String outputFileName = config.getOutputPattern()
.replace("{filename}", baseName)
.replace("{pipelineName}", config.getName())
.replace("{date}", LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
.replace("{time}", LocalTime.now().format(DateTimeFormatter.ofPattern("HHmmss")))
+ "." + extension;
return outputFileName;
}
private Path determineOutputPath(PipelineConfig config, Path dir) {
String outputDir = config.getOutputDir()
.replace("{outputFolder}", finishedFoldersDir)
.replace("{folderName}", dir.toString())
.replaceAll("\\\\?watchedFolders", "");
return Paths.get(outputDir).isAbsolute() ? Paths.get(outputDir) : Paths.get(".", outputDir);
}
private void deleteOriginalFiles(List<File> filesToProcess, Path processingDir) throws IOException {
for (File file : filesToProcess) {
Files.deleteIfExists(processingDir.resolve(file.getName()));
logger.info("Deleted original file: {}", file.getName());
}
}
private void moveFilesBack(List<File> filesToProcess, Path processingDir) {
for (File file : filesToProcess) {
try {
Files.move(processingDir.resolve(file.getName()), file.toPath());
logger.info("Moved file back to original location: {} , {}",file.toPath(), file.getName());
} catch (IOException e) {
logger.error("Error moving file back to original location: {}", file.getName(), e);
}
}
}
}

View File

@ -0,0 +1,332 @@
package stirling.software.SPDF.controller.api.pipeline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import jakarta.servlet.ServletContext;
import stirling.software.SPDF.SPdfApplication;
import stirling.software.SPDF.model.PipelineConfig;
import stirling.software.SPDF.model.PipelineOperation;
import stirling.software.SPDF.model.Role;
@Service
public class PipelineProcessor {
private static final Logger logger = LoggerFactory.getLogger(PipelineProcessor.class);
@Autowired
private ApiDocService apiDocService;
@Autowired(required=false)
private UserServiceInterface userService;
@Autowired
private ServletContext servletContext;
private String getApiKeyForUser() {
if (userService == null)
return "";
return userService.getApiKeyForUser(Role.INTERNAL_API_USER.getRoleId());
}
private String getBaseUrl() {
String contextPath = servletContext.getContextPath();
String port = SPdfApplication.getPort();
return "http://localhost:" + port + contextPath + "/";
}
List<Resource> runPipelineAgainstFiles(List<Resource> outputFiles, PipelineConfig config) throws Exception {
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
PrintStream logPrintStream = new PrintStream(logStream);
boolean hasErrors = false;
for (PipelineOperation pipelineOperation : config.getOperations()) {
String operation = pipelineOperation.getOperation();
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
logger.info("Running operation: {} isMultiInputOperation {}", operation, isMultiInputOperation);
Map<String, Object> parameters = pipelineOperation.getParameters();
String inputFileExtension = "";
//TODO
//if (operationNode.has("inputFileType")) {
// inputFileExtension = operationNode.get("inputFileType").asText();
//} else {
inputFileExtension = ".pdf";
//}
final String finalInputFileExtension = inputFileExtension;
String url = getBaseUrl() + operation;
List<Resource> newOutputFiles = new ArrayList<>();
if (!isMultiInputOperation) {
for (Resource file : outputFiles) {
boolean hasInputFileType = false;
if (file.getFilename().endsWith(inputFileExtension)) {
hasInputFileType = true;
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("fileInput", file);
for(Entry<String, Object> entry : parameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
ResponseEntity<byte[]> response = sendWebRequest(url, body);
// If the operation is filter and the response body is null or empty, skip this
// file
if (operation.startsWith("filter-")
&& (response.getBody() == null || response.getBody().length == 0)) {
logger.info("Skipping file due to failing {}", operation);
continue;
}
if (!response.getStatusCode().equals(HttpStatus.OK)) {
logPrintStream.println("Error: " + response.getBody());
hasErrors = true;
continue;
}
processOutputFiles(operation, file.getFilename(), response, newOutputFiles);
}
if (!hasInputFileType) {
logPrintStream.println(
"No files with extension " + inputFileExtension + " found for operation " + operation);
hasErrors = true;
}
outputFiles = newOutputFiles;
}
} else {
// Filter and collect all files that match the inputFileExtension
List<Resource> matchingFiles = outputFiles.stream()
.filter(file -> file.getFilename().endsWith(finalInputFileExtension))
.collect(Collectors.toList());
// Check if there are matching files
if (!matchingFiles.isEmpty()) {
// Create a new MultiValueMap for the request body
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
// Add all matching files to the body
for (Resource file : matchingFiles) {
body.add("fileInput", file);
}
for(Entry<String, Object> entry : parameters.entrySet()) {
body.add(entry.getKey(), entry.getValue());
}
ResponseEntity<byte[]> response = sendWebRequest(url, body);
// Handle the response
if (response.getStatusCode().equals(HttpStatus.OK)) {
processOutputFiles(operation, matchingFiles.get(0).getFilename(), response, newOutputFiles);
} else {
// Log error if the response status is not OK
logPrintStream.println("Error in multi-input operation: " + response.getBody());
hasErrors = true;
}
} else {
logPrintStream.println("No files with extension " + inputFileExtension + " found for multi-input operation " + operation);
hasErrors = true;
}
}
logPrintStream.close();
}
if (hasErrors) {
logger.error("Errors occurred during processing. Log: {}", logStream.toString());
}
return outputFiles;
}
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body ){
RestTemplate restTemplate = new RestTemplate();
// Set up headers, including API key
HttpHeaders headers = new HttpHeaders();
String apiKey = getApiKeyForUser();
headers.add("X-API-Key", apiKey);
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
// Create HttpEntity with the body and headers
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// Make the request to the REST endpoint
return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class);
}
private List<Resource> processOutputFiles(String operation, String fileName, ResponseEntity<byte[]> response, List<Resource> newOutputFiles) throws IOException{
// Define filename
String newFilename;
if ("auto-rename".equals(operation)) {
// If the operation is "auto-rename", generate a new filename.
// This is a simple example of generating a filename using current timestamp.
// Modify as per your needs.
newFilename = "file_" + System.currentTimeMillis();
} else {
// Otherwise, keep the original filename.
newFilename = fileName;
}
// Check if the response body is a zip file
if (isZip(response.getBody())) {
// Unzip the file and add all the files to the new output files
newOutputFiles.addAll(unzip(response.getBody()));
} else {
Resource outputResource = new ByteArrayResource(response.getBody()) {
@Override
public String getFilename() {
return newFilename;
}
};
newOutputFiles.add(outputResource);
}
return newOutputFiles;
}
List<Resource> generateInputFiles(File[] files) throws Exception {
if (files == null || files.length == 0) {
logger.info("No files");
return null;
}
List<Resource> outputFiles = new ArrayList<>();
for (File file : files) {
Path path = Paths.get(file.getAbsolutePath());
logger.info("Reading file: " + path); // debug statement
if (Files.exists(path)) {
Resource fileResource = new ByteArrayResource(Files.readAllBytes(path)) {
@Override
public String getFilename() {
return file.getName();
}
};
outputFiles.add(fileResource);
} else {
logger.info("File not found: " + path);
}
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
List<Resource> generateInputFiles(MultipartFile[] files) throws Exception {
if (files == null || files.length == 0) {
logger.info("No files");
return null;
}
List<Resource> outputFiles = new ArrayList<>();
for (MultipartFile file : files) {
Resource fileResource = new ByteArrayResource(file.getBytes()) {
@Override
public String getFilename() {
return file.getOriginalFilename();
}
};
outputFiles.add(fileResource);
}
logger.info("Files successfully loaded. Starting processing...");
return outputFiles;
}
private boolean isZip(byte[] data) {
if (data == null || data.length < 4) {
return false;
}
// Check the first four bytes of the data against the standard zip magic number
return data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04;
}
private List<Resource> unzip(byte[] data) throws IOException {
logger.info("Unzipping data of length: {}", data.length);
List<Resource> unzippedFiles = new ArrayList<>();
try (ByteArrayInputStream bais = new ByteArrayInputStream(data);
ZipInputStream zis = new ZipInputStream(bais)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int count;
while ((count = zis.read(buffer)) != -1) {
baos.write(buffer, 0, count);
}
final String filename = entry.getName();
Resource fileResource = new ByteArrayResource(baos.toByteArray()) {
@Override
public String getFilename() {
return filename;
}
};
// If the unzipped file is a zip file, unzip it
if (isZip(baos.toByteArray())) {
logger.info("File {} is a zip file. Unzipping...", filename);
unzippedFiles.addAll(unzip(baos.toByteArray()));
} else {
unzippedFiles.add(fileResource);
}
}
}
logger.info("Unzipping completed. {} files were unzipped.", unzippedFiles.size());
return unzippedFiles;
}
}

View File

@ -0,0 +1,4 @@
package stirling.software.SPDF.controller.api.pipeline;
public interface UserServiceInterface {
String getApiKeyForUser(String username);
}

View File

@ -1,4 +1,5 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -15,6 +16,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Controller @Controller
@ -46,14 +49,28 @@ public class AccountWebController {
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/addUsers") @GetMapping("/addUsers")
public String showAddUserForm(Model model, Authentication authentication) { public String showAddUserForm(Model model, Authentication authentication) {
List<User> allUsers = userRepository.findAll(); List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator();
while(iterator.hasNext()) {
User user = iterator.next();
if(user != null) {
for (Authority authority : user.getAuthorities()) {
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
iterator.remove();
break; // Break out of the inner loop once the user is removed
}
}
}
}
model.addAttribute("users", allUsers); model.addAttribute("users", allUsers);
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
return "addUsers"; return "addUsers";
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/account") @GetMapping("/account")
public String account(HttpServletRequest request, Model model, Authentication authentication) { public String account(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
@ -100,7 +117,7 @@ public class AccountWebController {
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@GetMapping("/change-creds") @GetMapping("/change-creds")
public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {

View File

@ -1,5 +1,6 @@
package stirling.software.SPDF.controller.web; package stirling.software.SPDF.controller.web;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
@ -41,31 +42,44 @@ public class GeneralWebController {
model.addAttribute("currentPage", "pipeline"); model.addAttribute("currentPage", "pipeline");
List<String> pipelineConfigs = new ArrayList<>(); List<String> pipelineConfigs = new ArrayList<>();
try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>();
List<Path> jsonFiles = paths
.filter(Files::isRegularFile) if(new File("./pipeline/defaultWebUIConfigs/").exists()) {
.filter(p -> p.toString().endsWith(".json")) try (Stream<Path> paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) {
.collect(Collectors.toList()); List<Path> jsonFiles = paths
.filter(Files::isRegularFile)
for (Path jsonFile : jsonFiles) { .filter(p -> p.toString().endsWith(".json"))
String content = Files.readString(jsonFile, StandardCharsets.UTF_8); .collect(Collectors.toList());
pipelineConfigs.add(content);
} for (Path jsonFile : jsonFiles) {
List<Map<String, String>> pipelineConfigsWithNames = new ArrayList<>(); String content = Files.readString(jsonFile, StandardCharsets.UTF_8);
for (String config : pipelineConfigs) { pipelineConfigs.add(content);
Map<String, Object> jsonContent = new ObjectMapper().readValue(config, new TypeReference<Map<String, Object>>(){}); }
String name = (String) jsonContent.get("name"); for (String config : pipelineConfigs) {
Map<String, String> configWithName = new HashMap<>(); Map<String, Object> jsonContent = new ObjectMapper().readValue(config, new TypeReference<Map<String, Object>>(){});
configWithName.put("json", config);
configWithName.put("name", name); String name = (String) jsonContent.get("name");
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", config);
configWithName.put("name", name);
pipelineConfigsWithNames.add(configWithName);
}
} catch (IOException e) {
e.printStackTrace();
}
}
if(pipelineConfigsWithNames.size() == 0) {
Map<String, String> configWithName = new HashMap<>();
configWithName.put("json", "");
configWithName.put("name", "No preloaded configs found");
pipelineConfigsWithNames.add(configWithName); pipelineConfigsWithNames.add(configWithName);
}
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
} catch (IOException e) {
e.printStackTrace();
} }
model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames);
model.addAttribute("pipelineConfigs", pipelineConfigs); model.addAttribute("pipelineConfigs", pipelineConfigs);

View File

@ -27,8 +27,8 @@ import stirling.software.SPDF.config.StartupApplicationListener;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@RestController @RestController
@RequestMapping("/api/v1") @RequestMapping("/api/v1/info")
@Tag(name = "API", description = "Info APIs") @Tag(name = "Info", description = "Info APIs")
public class MetricsController { public class MetricsController {

View File

@ -0,0 +1,42 @@
package stirling.software.SPDF.model;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.JsonNode;
public class ApiEndpoint {
private String name;
private Map<String, JsonNode> parameters;
private String description;
public ApiEndpoint(String name, JsonNode postNode) {
this.name = name;
this.parameters = new HashMap<>();
postNode.path("parameters").forEach(paramNode -> {
String paramName = paramNode.path("name").asText();
parameters.put(paramName, paramNode);
});
this.description = postNode.path("description").asText();
}
public boolean areParametersValid(Map<String, Object> providedParams) {
for (String requiredParam : parameters.keySet()) {
if (!providedParams.containsKey(requiredParam)) {
return false;
}
}
return true;
}
public String getDescription() {
return description;
}
@Override
public String toString() {
return "ApiEndpoint [name=" + name + ", parameters=" + parameters + "]";
}
}

View File

@ -106,6 +106,25 @@ public class ApplicationProperties {
private Boolean enableLogin; private Boolean enableLogin;
private Boolean csrfDisabled; private Boolean csrfDisabled;
private InitialLogin initialLogin; private InitialLogin initialLogin;
private int loginAttemptCount;
private long loginResetTimeMinutes;
public int getLoginAttemptCount() {
return loginAttemptCount;
}
public void setLoginAttemptCount(int loginAttemptCount) {
this.loginAttemptCount = loginAttemptCount;
}
public long getLoginResetTimeMinutes() {
return loginResetTimeMinutes;
}
public void setLoginResetTimeMinutes(long loginResetTimeMinutes) {
this.loginResetTimeMinutes = loginResetTimeMinutes;
}
public InitialLogin getInitialLogin() { public InitialLogin getInitialLogin() {
return initialLogin != null ? initialLogin : new InitialLogin(); return initialLogin != null ? initialLogin : new InitialLogin();
@ -175,6 +194,19 @@ public class ApplicationProperties {
private String rootURIPath; private String rootURIPath;
private String customStaticFilePath; private String customStaticFilePath;
private Integer maxFileSize; private Integer maxFileSize;
private Boolean enableAlphaFunctionality;
public Boolean getEnableAlphaFunctionality() {
return enableAlphaFunctionality;
}
public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) {
this.enableAlphaFunctionality = enableAlphaFunctionality;
}
public String getDefaultLocale() { public String getDefaultLocale() {
return defaultLocale; return defaultLocale;
@ -218,12 +250,13 @@ public class ApplicationProperties {
@Override @Override
public String toString() { public String toString() {
return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + ", rootURIPath=" return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility
+ rootURIPath + ", customStaticFilePath=" + customStaticFilePath + ", maxFileSize=" + maxFileSize + ", rootURIPath=" + rootURIPath + ", customStaticFilePath=" + customStaticFilePath
+ "]"; + ", maxFileSize=" + maxFileSize + ", enableAlphaFunctionality=" + enableAlphaFunctionality + "]";
} }
} }
public static class Ui { public static class Ui {

View File

@ -0,0 +1,27 @@
package stirling.software.SPDF.model;
public class AttemptCounter {
private int attemptCount;
private long lastAttemptTime;
public AttemptCounter() {
this.attemptCount = 1;
this.lastAttemptTime = System.currentTimeMillis();
}
public void increment() {
this.attemptCount++;
this.lastAttemptTime = System.currentTimeMillis();
}
public int getAttemptCount() {
return attemptCount;
}
public long getlastAttemptTime() {
return lastAttemptTime;
}
public boolean shouldReset(long ATTEMPT_INCREMENT_TIME) {
return System.currentTimeMillis() - lastAttemptTime > ATTEMPT_INCREMENT_TIME;
}
}

View File

@ -14,8 +14,13 @@ public enum Role {
EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20), EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20),
// 0 API calls per day and 20 web calls // 0 API calls per day and 20 web calls
WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20); WEB_ONLY_USER("ROLE_WEB_ONLY_USER", 0, 20),
INTERNAL_API_USER("STIRLING-PDF-BACKEND-API-USER", Integer.MAX_VALUE, Integer.MAX_VALUE),
DEMO_USER("ROLE_DEMO_USER", 100, 100);
private final String roleId; private final String roleId;
private final int apiCallsPerDay; private final int apiCallsPerDay;
private final int webCallsPerDay; private final int webCallsPerDay;

View File

@ -13,8 +13,8 @@ import lombok.NoArgsConstructor;
public class HandleDataRequest { public class HandleDataRequest {
@Schema(description = "The input files") @Schema(description = "The input files")
private MultipartFile[] fileInputs; private MultipartFile[] fileInput;
@Schema(description = "JSON String") @Schema(description = "JSON String")
private String jsonString; private String json;
} }

View File

@ -5,6 +5,7 @@ import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -18,7 +19,7 @@ public class PDFWithPageNums extends PDFFile {
@Schema(description = "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"") @Schema(description = "The pages to select, Supports ranges (e.g., '1,3,5-9'), or 'all' or functions in the format 'an+b' where 'a' is the multiplier of the page number 'n', and 'b' is a constant (e.g., '2n+1', '3n', '6n-5')\"")
private String pageNumbers; private String pageNumbers;
@Hidden
public List<Integer> getPageNumbersList(){ public List<Integer> getPageNumbersList(){
int pageCount = 0; int pageCount = 0;
try { try {
@ -30,6 +31,8 @@ public class PDFWithPageNums extends PDFFile {
return GeneralUtils.parsePageString(pageNumbers, pageCount); return GeneralUtils.parsePageString(pageNumbers, pageCount);
} }
@Hidden
public List<Integer> getPageNumbersList(PDDocument doc){ public List<Integer> getPageNumbersList(PDDocument doc){
int pageCount = 0; int pageCount = 0;
pageCount = doc.getNumberOfPages(); pageCount = doc.getNumberOfPages();

View File

@ -4,22 +4,17 @@ import java.awt.Graphics;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import javax.imageio.IIOImage; import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader; import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.ImageWriteParam; import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream; import javax.imageio.stream.ImageOutputStream;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;

View File

@ -0,0 +1,16 @@
package stirling.software.SPDF.utils;
public class RequestUriUtils {
public static boolean isStaticResource(String requestURI) {
return requestURI.startsWith("/css/")
|| requestURI.startsWith("/js/")
|| requestURI.startsWith("/images/")
|| requestURI.startsWith("/public/")
|| requestURI.startsWith("/pdfjs/")
|| requestURI.endsWith(".svg");
}
}

View File

@ -0,0 +1,6 @@
____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____
/ ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___|
\___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_
___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _|
|____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_|
Powered by Spring Boot ${spring-boot.version}

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
@ -255,6 +256,10 @@ home.removeBlanks.title=إزالة الصفحات الفارغة
home.removeBlanks.desc=يكتشف ويزيل الصفحات الفارغة من المستند home.removeBlanks.desc=يكتشف ويزيل الصفحات الفارغة من المستند
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=قارن home.compare.title=قارن
home.compare.desc=يقارن ويظهر الاختلافات بين 2 من مستندات PDF home.compare.desc=يقارن ويظهر الاختلافات بين 2 من مستندات PDF
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=النسبة المئوية للصفحة التي
removeBlanks.submit=إزالة الفراغات removeBlanks.submit=إزالة الفراغات
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=يقارن compare.title=يقارن
compare.header=قارن ملفات PDF compare.header=قارن ملفات PDF

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Роля
adminUserSettings.actions=Действия adminUserSettings.actions=Действия
adminUserSettings.apiUser=Ограничен API потребител adminUserSettings.apiUser=Ограничен API потребител
adminUserSettings.webOnlyUser=Само за уеб-потребител adminUserSettings.webOnlyUser=Само за уеб-потребител
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане
adminUserSettings.submit=Съхранете потребителя adminUserSettings.submit=Съхранете потребителя
@ -255,6 +256,10 @@ home.removeBlanks.title=Премахване на празни страници
home.removeBlanks.desc=Открива и премахва празни страници от документ home.removeBlanks.desc=Открива и премахва празни страници от документ
removeBlanks.tags=почистване,рационализиране,без съдържание,организиране removeBlanks.tags=почистване,рационализиране,без съдържание,организиране
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Сравнете home.compare.title=Сравнете
home.compare.desc=Сравнява и показва разликите между 2 PDF документа home.compare.desc=Сравнява и показва разликите между 2 PDF документа
compare.tags=разграничаване,контраст,промени,анализ compare.tags=разграничаване,контраст,промени,анализ
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Процент от страницата, коят
removeBlanks.submit=Премахване на празни места removeBlanks.submit=Премахване на празни места
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Сравнявай compare.title=Сравнявай
compare.header=Сравнявай PDF-и compare.header=Сравнявай PDF-и

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rol
adminUserSettings.actions=Accions adminUserSettings.actions=Accions
adminUserSettings.apiUser=Usuari amb API limitada adminUserSettings.apiUser=Usuari amb API limitada
adminUserSettings.webOnlyUser=Usuari només WEB adminUserSettings.webOnlyUser=Usuari només WEB
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Desar Usuari adminUserSettings.submit=Desar Usuari
@ -255,6 +256,10 @@ home.removeBlanks.title=Elimina les pàgines en blanc
home.removeBlanks.desc=Detecta i elimina les pàgines en blanc d'un document home.removeBlanks.desc=Detecta i elimina les pàgines en blanc d'un document
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Compara home.compare.title=Compara
home.compare.desc=Compara i mostra les diferències entre 2 documents PDF home.compare.desc=Compara i mostra les diferències entre 2 documents PDF
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentatge de pàgina que ha de ser blanca per el
removeBlanks.submit=Elimina els espais en blanc removeBlanks.submit=Elimina els espais en blanc
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Comparar compare.title=Comparar
compare.header=Compara PDF compare.header=Compara PDF

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rolle
adminUserSettings.actions=Aktion adminUserSettings.actions=Aktion
adminUserSettings.apiUser=Eingeschränkter API-Benutzer adminUserSettings.apiUser=Eingeschränkter API-Benutzer
adminUserSettings.webOnlyUser=Nur Web-Benutzer adminUserSettings.webOnlyUser=Nur Web-Benutzer
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern adminUserSettings.forceChange=Benutzer dazu zwingen, Benutzernamen/Passwort bei der Anmeldung zu ändern
adminUserSettings.submit=Benutzer speichern adminUserSettings.submit=Benutzer speichern
@ -255,6 +256,10 @@ home.removeBlanks.title=Leere Seiten entfernen
home.removeBlanks.desc=Erkennt und entfernt leere Seiten aus einem Dokument home.removeBlanks.desc=Erkennt und entfernt leere Seiten aus einem Dokument
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Vergleichen home.compare.title=Vergleichen
home.compare.desc=Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an home.compare.desc=Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Prozentsatz der Seite, die weiß sein muss, um ent
removeBlanks.submit=Leere Seiten entfernen removeBlanks.submit=Leere Seiten entfernen
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Vergleichen compare.title=Vergleichen
compare.header=PDFs vergleichen compare.header=PDFs vergleichen

View File

@ -119,6 +119,7 @@ adminUserSettings.role=\u03A1\u03CC\u03BB\u03BF\u03C2
adminUserSettings.actions=\u0395\u03BD\u03AD\u03C1\u03B3\u03B5\u03B9\u03B5\u03C2 adminUserSettings.actions=\u0395\u03BD\u03AD\u03C1\u03B3\u03B5\u03B9\u03B5\u03C2
adminUserSettings.apiUser=\u03A0\u03B5\u03C1\u03B9\u03BF\u03C1\u03B9\u03C3\u03BC\u03AD\u03BD\u03BF\u03C2 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03B3\u03B9\u03B1 \u03B4\u03B9\u03B5\u03C0\u03B1\u03C6\u03AE \u03C0\u03C1\u03BF\u03B3\u03C1\u03B1\u03BC\u03BC\u03B1\u03C4\u03B9\u03C3\u03BC\u03BF\u03CD \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03CE\u03BD (API User) adminUserSettings.apiUser=\u03A0\u03B5\u03C1\u03B9\u03BF\u03C1\u03B9\u03C3\u03BC\u03AD\u03BD\u03BF\u03C2 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03B3\u03B9\u03B1 \u03B4\u03B9\u03B5\u03C0\u03B1\u03C6\u03AE \u03C0\u03C1\u03BF\u03B3\u03C1\u03B1\u03BC\u03BC\u03B1\u03C4\u03B9\u03C3\u03BC\u03BF\u03CD \u03B5\u03C6\u03B1\u03C1\u03BC\u03BF\u03B3\u03CE\u03BD (API User)
adminUserSettings.webOnlyUser=\u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03BC\u03CC\u03BD\u03BF \u0399\u03C3\u03C4\u03BF\u03CD adminUserSettings.webOnlyUser=\u03A7\u03C1\u03AE\u03C3\u03C4\u03B7\u03C2 \u03BC\u03CC\u03BD\u03BF \u0399\u03C3\u03C4\u03BF\u03CD
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=\u0391\u03BD\u03B1\u03B3\u03BA\u03AC\u03C3\u03C4\u03B5 \u03C4\u03BF\u03BD \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7 \u03BD\u03B1 \u03B1\u03BB\u03BB\u03AC\u03BE\u03B5\u03B9 \u03C4\u03BF \u03CC\u03BD\u03BF\u03BC\u03B1 \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7/\u03BA\u03C9\u03B4\u03B9\u03BA\u03CC \u03C0\u03C1\u03CC\u03C3\u03B2\u03B1\u03C3\u03B7\u03C2 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7 \u03C3\u03CD\u03BD\u03B4\u03B5\u03C3\u03B7 adminUserSettings.forceChange=\u0391\u03BD\u03B1\u03B3\u03BA\u03AC\u03C3\u03C4\u03B5 \u03C4\u03BF\u03BD \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7 \u03BD\u03B1 \u03B1\u03BB\u03BB\u03AC\u03BE\u03B5\u03B9 \u03C4\u03BF \u03CC\u03BD\u03BF\u03BC\u03B1 \u03C7\u03C1\u03AE\u03C3\u03C4\u03B7/\u03BA\u03C9\u03B4\u03B9\u03BA\u03CC \u03C0\u03C1\u03CC\u03C3\u03B2\u03B1\u03C3\u03B7\u03C2 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7 \u03C3\u03CD\u03BD\u03B4\u03B5\u03C3\u03B7
adminUserSettings.submit=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7 adminUserSettings.submit=\u0391\u03C0\u03BF\u03B8\u03AE\u03BA\u03B5\u03C5\u03C3\u03B7 \u03A7\u03C1\u03AE\u03C3\u03C4\u03B7
@ -255,6 +256,10 @@ home.removeBlanks.title=\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03BA\
home.removeBlanks.desc=\u0391\u03BD\u03AF\u03C7\u03B5\u03C5\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B1\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03BA\u03B5\u03BD\u03CE\u03BD \u03C3\u03B5\u03BB\u03AF\u03B4\u03C9\u03BD \u03B1\u03C0\u03CC \u03AD\u03BD\u03B1 \u03AD\u03B3\u03B3\u03C1\u03B1\u03C6\u03BF home.removeBlanks.desc=\u0391\u03BD\u03AF\u03C7\u03B5\u03C5\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B1\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u03BA\u03B5\u03BD\u03CE\u03BD \u03C3\u03B5\u03BB\u03AF\u03B4\u03C9\u03BD \u03B1\u03C0\u03CC \u03AD\u03BD\u03B1 \u03AD\u03B3\u03B3\u03C1\u03B1\u03C6\u03BF
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 home.compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7
home.compare.desc=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B5\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7 \u03C4\u03C9\u03BD \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03CE\u03BD \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD \u03B4\u03CD\u03BF PDF \u03B1\u03C1\u03C7\u03B5\u03AF\u03C9\u03BD home.compare.desc=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 \u03BA\u03B1\u03B9 \u03B5\u03BC\u03C6\u03AC\u03BD\u03B9\u03C3\u03B7 \u03C4\u03C9\u03BD \u03B4\u03B9\u03B1\u03C6\u03BF\u03C1\u03CE\u03BD \u03BC\u03B5\u03C4\u03B1\u03BE\u03CD \u03B4\u03CD\u03BF PDF \u03B1\u03C1\u03C7\u03B5\u03AF\u03C9\u03BD
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=\u03A4\u03BF \u03C0\u03BF\u03C3\u03BF\u03C3\u03C4\
removeBlanks.submit=\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u039A\u03B5\u03BD\u03CE\u03BD removeBlanks.submit=\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7 \u039A\u03B5\u03BD\u03CE\u03BD
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7
compare.header=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 PDFs compare.header=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 PDFs

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange = Force user to change username/password on login adminUserSettings.forceChange = Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rol
adminUserSettings.actions=Acciones adminUserSettings.actions=Acciones
adminUserSettings.apiUser=Usuario limitado de API adminUserSettings.apiUser=Usuario limitado de API
adminUserSettings.webOnlyUser=Usuario solo web adminUserSettings.webOnlyUser=Usuario solo web
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso adminUserSettings.forceChange=Forzar usuario a cambiar usuario/contraseña en el acceso
adminUserSettings.submit=Guardar Usuario adminUserSettings.submit=Guardar Usuario
@ -255,6 +256,10 @@ home.removeBlanks.title=Eliminar páginas en blanco
home.removeBlanks.desc=Detectar y eliminar páginas en blanco de un documento home.removeBlanks.desc=Detectar y eliminar páginas en blanco de un documento
removeBlanks.tags=limpieza,dinámica,sin contenido,organizar removeBlanks.tags=limpieza,dinámica,sin contenido,organizar
home.removeAnnotations.title=Eliminar Anotaciones
home.removeAnnotations.desc=Eliminar todos los comentarios/anotaciones de un PDF
removeAnnotations.tags=comentarios,subrayar,notas,margen,eliminar
home.compare.title=Comparar home.compare.title=Comparar
home.compare.desc=Comparar y mostrar las diferencias entre 2 documentos PDF home.compare.desc=Comparar y mostrar las diferencias entre 2 documentos PDF
compare.tags=diferenciar,contrastar,cambios,análisis compare.tags=diferenciar,contrastar,cambios,análisis
@ -350,7 +355,7 @@ home.overlay-pdfs.title=Superponer PDFs
home.overlay-pdfs.desc=Superponer PDFs encima de otro PDF home.overlay-pdfs.desc=Superponer PDFs encima de otro PDF
overlay-pdfs.tags=Superponer overlay-pdfs.tags=Superponer
home.split-by-sections.title=Dividir PDF por Seccioned home.split-by-sections.title=Dividir PDF por Secciones
home.split-by-sections.desc=Dividir cada página de un PDF en secciones verticales y horizontales más pequeñas home.split-by-sections.desc=Dividir cada página de un PDF en secciones verticales y horizontales más pequeñas
split-by-sections.tags=Dividir sección, Dividir, Personalizar split-by-sections.tags=Dividir sección, Dividir, Personalizar
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Porcentaje de página que debe ser blanca para ser
removeBlanks.submit=Eliminar espacios en blanco removeBlanks.submit=Eliminar espacios en blanco
#removeAnnotations
removeAnnotations.title=Eliminar anotaciones
removeAnnotations.header=Eliminar anotaciones
removeAnnotations.submit=Eliminar
#compare #compare
compare.title=Comparar compare.title=Comparar
compare.header=Comparar archivos PDF compare.header=Comparar archivos PDF
@ -861,7 +872,7 @@ split-by-size-or-count.submit=Enviar
#overlay-pdfs #overlay-pdfs
overlay-pdfs.header=Superponer archivos PDF overlay-pdfs.header=Superponer archivos PDF
overlay-pdfs.baseFile.label=Selleccione archivo PDF de base overlay-pdfs.baseFile.label=Seleccione archivo PDF de base
overlay-pdfs.overlayFiles.label=Seleccione archivos PDF a superponer overlay-pdfs.overlayFiles.label=Seleccione archivos PDF a superponer
overlay-pdfs.mode.label=Seleccione modo de superposición overlay-pdfs.mode.label=Seleccione modo de superposición
overlay-pdfs.mode.sequential=Superposición Sequencial overlay-pdfs.mode.sequential=Superposición Sequencial

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rol
adminUserSettings.actions=Ekintzak adminUserSettings.actions=Ekintzak
adminUserSettings.apiUser=APIren erabiltzaile mugatua adminUserSettings.apiUser=APIren erabiltzaile mugatua
adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Gorde Erabiltzailea adminUserSettings.submit=Gorde Erabiltzailea
@ -255,6 +256,10 @@ home.removeBlanks.title=Ezabatu orrialde zuriak
home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Konparatu home.compare.title=Konparatu
home.compare.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak home.compare.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Zuria izan behar den orriaren ehunekoa ezabatua iz
removeBlanks.submit=Ezabatu zuriuneak removeBlanks.submit=Ezabatu zuriuneak
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Konparatu compare.title=Konparatu
compare.header=Konparatu PDF fitxategiak compare.header=Konparatu PDF fitxategiak

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rôle
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Utilisateur API limité adminUserSettings.apiUser=Utilisateur API limité
adminUserSettings.webOnlyUser=Utilisateur Web uniquement adminUserSettings.webOnlyUser=Utilisateur Web uniquement
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Forcer l\u2019utilisateur à changer son nom d\u2019utilisateur/mot de passe lors de la connexion adminUserSettings.forceChange=Forcer l\u2019utilisateur à changer son nom d\u2019utilisateur/mot de passe lors de la connexion
adminUserSettings.submit=Ajouter adminUserSettings.submit=Ajouter
@ -255,6 +256,10 @@ home.removeBlanks.title=Supprimer les pages vierges
home.removeBlanks.desc=Détectez et supprimez les pages vierges d\u2019un PDF. home.removeBlanks.desc=Détectez et supprimez les pages vierges d\u2019un PDF.
removeBlanks.tags=pages vierges,supprimer,nettoyer,cleanup,streamline,non-content,organize removeBlanks.tags=pages vierges,supprimer,nettoyer,cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Comparer home.compare.title=Comparer
home.compare.desc=Comparez et visualisez les différences entre deux PDF. home.compare.desc=Comparez et visualisez les différences entre deux PDF.
compare.tags=comparer,analyser,differentiate,contrast,changes,analysis compare.tags=comparer,analyser,differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Pourcentage de la page qui doit contenir des pixel
removeBlanks.submit=Supprimer les pages vierges removeBlanks.submit=Supprimer les pages vierges
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Comparer compare.title=Comparer
compare.header=Comparer compare.header=Comparer

View File

@ -1,7 +1,7 @@
########### ###########
# Generic # # Generic #
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr=left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
pdfPrompt=पीडीएफ़(फ़ाइलें) चुनें pdfPrompt=पीडीएफ़(फ़ाइलें) चुनें
@ -92,7 +92,7 @@ account.title=खाता सेटिंग्स
account.accountSettings=खाता सेटिंग्स account.accountSettings=खाता सेटिंग्स
account.adminSettings=व्यवस्थापक सेटिंग्स - उपयोगकर्ताओं को देखें और जोड़ें account.adminSettings=व्यवस्थापक सेटिंग्स - उपयोगकर्ताओं को देखें और जोड़ें
account.userControlSettings=उपयोगकर्ता नियंत्रण सेटिंग्स account.userControlSettings=उपयोगकर्ता नियंत्रण सेटिंग्स
account.changeUsername=नया उपयोगकर्ता नाम account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें
account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें
account.password=पासवर्ड पुष्टि account.password=पासवर्ड पुष्टि
account.oldPassword=पुराना पासवर्ड account.oldPassword=पुराना पासवर्ड
@ -119,6 +119,7 @@ adminUserSettings.role=रोल
adminUserSettings.actions=क्रियाएँ adminUserSettings.actions=क्रियाएँ
adminUserSettings.apiUser=सीमित API उपयोगकर्ता adminUserSettings.apiUser=सीमित API उपयोगकर्ता
adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=उपयोगकर्ता को लॉगिन पर उपयोगकर्ता नाम/पासवर्ड बदलने के लिए मजबूर करें adminUserSettings.forceChange=उपयोगकर्ता को लॉगिन पर उपयोगकर्ता नाम/पासवर्ड बदलने के लिए मजबूर करें
adminUserSettings.submit=उपयोगकर्ता को सहेजें adminUserSettings.submit=उपयोगकर्ता को सहेजें
@ -175,24 +176,24 @@ home.permissions.title=अनुमतियाँ बदलें
home.permissions.desc=अपने पीडीएफ़ दस्तावेज़ की अनुमतियाँ बदलें home.permissions.desc=अपने पीडीएफ़ दस्तावेज़ की अनुमतियाँ बदलें
permissions.tags=पढ़ें, लिखें, संपादित करें, प्रिंट permissions.tags=पढ़ें, लिखें, संपादित करें, प्रिंट
home.removePages.title=हटाएं home.removePages.title=हटाएं
home.removePages.desc=अपने पीडीएफ़ दस्तावेज़ से अनचाहे पृष्ठों को हटाएं। home.removePages.desc=अपने पीडीएफ़ दस्तावेज़ से अनचाहे पृष्ठों को हटाएं।
removePages.tags=पृष्ठ हटाएं, पृष्ठ मिटाएं removePages.tags=पृष्ठ हटाएं, पृष्ठ मिटाएं
home.addPassword.title=पासवर्ड जोड़ें home.addPassword.title=पासवर्ड जोड़ें
home.addPassword.desc=अपने पीडीएफ़ दस्तावेज़ को एक पासवर्ड से एन्क्रिप्ट करें। home.addPassword.desc=अपने पीडीएफ़ दस्तावेज़ को एक पासवर्ड से एन्क्रिप्ट करें।
addPassword.tags=सुरक्षित, सुरक्षा addPassword.tags=सुरक्षित, सुरक्षा
home.removePassword.title=पासवर्ड हटाएं home.removePassword.title=पासवर्ड हटाएं
home.removePassword.desc=अपने पीडीएफ़ दस्तावेज़ से पासवर्ड सुरक्षा को हटाएं। home.removePassword.desc=अपने पीडीएफ़ दस्तावेज़ से पासवर्ड सुरक्षा को हटाएं।
removePassword.tags=सुरक्षित, डिक्रिप्ट, सुरक्षा, पासवर्ड हटाएं, पासवर्ड मिटाएं removePassword.tags=सुरक्षित, डिक्रिप्ट, सुरक्षा, पासवर्ड हटाएं, पासवर्ड मिटाएं
home.compressPdfs.title=Compress
home.compressPdfs.desc=Compress PDFs to reduce their file size.
compressPdfs.tags=squish,small,tiny
home.compressPdfs.title=संकुचित करें (कम्प्रेस)
home.compressPdfs.title=कम्प्रेस
home.compressPdfs.desc=फ़ाइल का आकार कम करने के लिए PDF को कम्प्रेस करें। home.compressPdfs.desc=फ़ाइल का आकार कम करने के लिए PDF को कम्प्रेस करें।
compressPdfs.tags=स्क्विश, छोटा, छोटा compressPdfs.tags=स्क्विश, छोटा, छोटा
home.changeMetadata.title=मेटाडेटा बदलें home.changeMetadata.title=मेटाडेटा बदलें
home.changeMetadata.desc=PDF दस्तावेज़ से मेटाडेटा बदलें/हटाएं/जोड़ें। home.changeMetadata.desc=PDF दस्तावेज़ से मेटाडेटा बदलें/हटाएं/जोड़ें।
changeMetadata.tags=शीर्षक, लेखक, तारीख, निर्माण, समय, प्रकाशक, उत्पादक, आँकड़े changeMetadata.tags=शीर्षक, लेखक, तारीख, निर्माण, समय, प्रकाशक, उत्पादक, आँकड़े
@ -205,6 +206,7 @@ home.ocr.title=OCR / स्कैन को साफ करें
home.ocr.desc=स्कैन को साफ करता है और पीडीएफ़ में छवियों से पाठ को पहचानता है और टेक्स्ट के रूप में फिर से जोड़ता है। home.ocr.desc=स्कैन को साफ करता है और पीडीएफ़ में छवियों से पाठ को पहचानता है और टेक्स्ट के रूप में फिर से जोड़ता है।
ocr.tags=पहचान, टेक्स्ट, छवि, स्कैन, पढ़ें, पहचान, पता लगाना, संपादनीय ocr.tags=पहचान, टेक्स्ट, छवि, स्कैन, पढ़ें, पहचान, पता लगाना, संपादनीय
home.extractImages.title=छवियां निकालें home.extractImages.title=छवियां निकालें
home.extractImages.desc=पीडीएफ़ से सभी छवियों को निकालता है और उन्हें ज़िप में सहेजता है home.extractImages.desc=पीडीएफ़ से सभी छवियों को निकालता है और उन्हें ज़िप में सहेजता है
extractImages.tags=चित्र, फोटो, सहेजें, संग्रह, ज़िप, कैप्चर, ग्रैब extractImages.tags=चित्र, फोटो, सहेजें, संग्रह, ज़िप, कैप्चर, ग्रैब
@ -213,7 +215,6 @@ home.pdfToPDFA.title=PDF से PDF/A में
home.pdfToPDFA.desc=लंबे समय के लिए स्टोरेज के लिए पीडीएफ़ को पीडीएफ़/ए में रूपांतरित करें home.pdfToPDFA.desc=लंबे समय के लिए स्टोरेज के लिए पीडीएफ़ को पीडीएफ़/ए में रूपांतरित करें
pdfToPDFA.tags=संग्रह, लंबे समय के लिए, मानक, परिवर्तन, स्टोरेज, संरक्षण pdfToPDFA.tags=संग्रह, लंबे समय के लिए, मानक, परिवर्तन, स्टोरेज, संरक्षण
home.PDFToWord.title=PDF से वर्ड में home.PDFToWord.title=PDF से वर्ड में
home.PDFToWord.desc=PDF को वर्ड प्रारूपों में रूपांतरित करें (DOC, DOCX और ODT) home.PDFToWord.desc=PDF को वर्ड प्रारूपों में रूपांतरित करें (DOC, DOCX और ODT)
PDFToWord.tags=doc,docx,odt,word,परिवर्तन,प्रारूप,रूपांतरण,ऑफिस,माइक्रोसॉफ्ट,डॉक फ़ाइल PDFToWord.tags=doc,docx,odt,word,परिवर्तन,प्रारूप,रूपांतरण,ऑफिस,माइक्रोसॉफ्ट,डॉक फ़ाइल
@ -230,6 +231,7 @@ home.PDFToHTML.title=PDF से HTML में
home.PDFToHTML.desc=PDF को HTML प्रारूप में रूपांतरित करें home.PDFToHTML.desc=PDF को HTML प्रारूप में रूपांतरित करें
PDFToHTML.tags=वेब सामग्री, ब्राउज़र अनुकूल PDFToHTML.tags=वेब सामग्री, ब्राउज़र अनुकूल
home.PDFToXML.title=PDF से XML में home.PDFToXML.title=PDF से XML में
home.PDFToXML.desc=PDF को XML प्रारूप में रूपांतरित करें home.PDFToXML.desc=PDF को XML प्रारूप में रूपांतरित करें
PDFToXML.tags=डेटा-निकालन, संरचित सामग्री, अंतरसंवाद, परिवर्तन, रूपांतरण PDFToXML.tags=डेटा-निकालन, संरचित सामग्री, अंतरसंवाद, परिवर्तन, रूपांतरण
@ -238,7 +240,6 @@ home.ScannerImageSplit.title=स्कैन की गई फोटो का
home.ScannerImageSplit.desc=एक फोटो/PDF के भीतर से कई फोटो को विभाजित करता है home.ScannerImageSplit.desc=एक फोटो/PDF के भीतर से कई फोटो को विभाजित करता है
ScannerImageSplit.tags=अलग, ऑटो-डिटेक्ट, स्कैन, मल्टी-फोटो, संगठित ScannerImageSplit.tags=अलग, ऑटो-डिटेक्ट, स्कैन, मल्टी-फोटो, संगठित
home.sign.title=हस्ताक्षर home.sign.title=हस्ताक्षर
home.sign.desc=हस्ताक्षर को ड्राइंग, पाठ या छवि के रूप में पीडीएफ़ में जोड़ता है। home.sign.desc=हस्ताक्षर को ड्राइंग, पाठ या छवि के रूप में पीडीएफ़ में जोड़ता है।
sign.tags=अधिकृत करें, आदेश, ड्राइंग-हस्ताक्षर, पाठ-हस्ताक्षर, छवि-हस्ताक्षर sign.tags=अधिकृत करें, आदेश, ड्राइंग-हस्ताक्षर, पाठ-हस्ताक्षर, छवि-हस्ताक्षर
@ -331,10 +332,10 @@ home.PdfToSinglePage.title=पीडीएफ़ से एक बड़े प
home.PdfToSinglePage.desc=सभी पीडीएफ़ पेजों को एक बड़े एकल पृष्ठ में मर्ज करता है home.PdfToSinglePage.desc=सभी पीडीएफ़ पेजों को एक बड़े एकल पृष्ठ में मर्ज करता है
PdfToSinglePage.tags=एकल पृष्ठ PdfToSinglePage.tags=एकल पृष्ठ
home.showJS.title=जावास्क्रिप्ट दिखाएं home.showJS.title=जावास्क्रिप्ट दिखाएं
home.showJS.desc=पीडीएफ़ में डाला गया कोई भी जावास्क्रिप्ट खोजता है और प्रदर्शित करता है home.showJS.desc=पीडीएफ़ में डाला गया कोई भी जावास्क्रिप्ट खोजता है और प्रदर्शित करता है
showJS.tags=जेएस showJS.tags=गोपनीयकरण, छिपाना, काला करना, काला, मार्कर, छिपा हुआ
home.autoRedact.title=स्वतः गोपनीयकरण home.autoRedact.title=स्वतः गोपनीयकरण
home.autoRedact.desc=प्रविष्ट पाठ के आधार पर पीडीएफ़ में पाठ को स्वतः गोपनीयकरित(काला करें) home.autoRedact.desc=प्रविष्ट पाठ के आधार पर पीडीएफ़ में पाठ को स्वतः गोपनीयकरित(काला करें)
@ -462,9 +463,9 @@ addPageNumbers.submit=पृष्ठ संख्या जोड़ें
#auto-rename #auto-rename
auto-rename.title=Auto Rename auto-rename.title=स्वतः नाम परिवर्तन (खुद ब खुद नाम बदलें)
auto-rename.header=Auto Rename PDF auto-rename.header=स्वतः नाम परिवर्तन पीडीएफ़
auto-rename.submit=Auto Rename auto-rename.submit=स्वतः नाम परिवर्तन
#adjustContrast #adjustContrast
@ -508,6 +509,7 @@ pageLayout.pagesPerSheet=प्रति पृष्ठ पेज:
pageLayout.addBorder=सीमा जोड़ें pageLayout.addBorder=सीमा जोड़ें
pageLayout.submit=प्रस्तुत क pageLayout.submit=प्रस्तुत क
#scalePages #scalePages
scalePages.title=पृष्ठ-स्केल समायोजित करें scalePages.title=पृष्ठ-स्केल समायोजित करें
scalePages.header=पृष्ठ-स्केल समायोजित करें scalePages.header=पृष्ठ-स्केल समायोजित करें
@ -516,7 +518,6 @@ scalePages.scaleFactor=पृष्ठ का ज़ूम स्तर (क्
scalePages.submit=प्रस्तुत करें scalePages.submit=प्रस्तुत करें
#certSign #certSign
certSign.title=प्रमाणपत्र साइनिंग certSign.title=प्रमाणपत्र साइनिंग
certSign.header=अपने प्रमाणपत्र के साथ एक पीडीएफ़ पर हस्ताक्षर करें (काम जारी है) certSign.header=अपने प्रमाणपत्र के साथ एक पीडीएफ़ पर हस्ताक्षर करें (काम जारी है)
@ -591,7 +592,7 @@ ScannerImageSplit.selectText.8=फोटो के लिए न्यूनत
ScannerImageSplit.selectText.9=बॉर्डर का आकार: ScannerImageSplit.selectText.9=बॉर्डर का आकार:
ScannerImageSplit.selectText.10=निकालने और जोड़ने के लिए जोड़ा जाने वाला बॉर्डर का आकार सेट करता है ताकि आउटपुट में सफेद बॉर्डर न आए (डिफ़ॉल्ट: 1)। ScannerImageSplit.selectText.10=निकालने और जोड़ने के लिए जोड़ा जाने वाला बॉर्डर का आकार सेट करता है ताकि आउटपुट में सफेद बॉर्डर न आए (डिफ़ॉल्ट: 1)।
#OCR #OCR
ocr.title=OCR / स्कैन सफाई ocr.title=OCR / स्कैन सफाई
ocr.header=स्कैन सफाई / OCR (ऑप्टिकल कैरेक्टर रिकग्निशन) ocr.header=स्कैन सफाई / OCR (ऑप्टिकल कैरेक्टर रिकग्निशन)
@ -635,7 +636,7 @@ compress.selectText.1=मैनुअल मोड - 1 से 4 तक
compress.selectText.2=अनुकूलन स्तर: compress.selectText.2=अनुकूलन स्तर:
compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक) compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक)
compress.selectText.4=स्वत: मोड - निर्धारित आकार पाने के लिए गुणवत्ता को स्वत: समायोजित करता है compress.selectText.4=स्वत: मोड - निर्धारित आकार पाने के लिए गुणवत्ता को स्वत: समायोजित करता है
compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB) compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB)
compress.submit=संकुचित करें compress.submit=संकुचित करें
@ -710,8 +711,8 @@ imageToPDF.selectText.2=पीडीएफ को ऑटो रोटेट क
imageToPDF.selectText.3=मल्टी फ़ाइल तर्क (केवल यदि कई छवियों के साथ काम किया जा रहा है) imageToPDF.selectText.3=मल्टी फ़ाइल तर्क (केवल यदि कई छवियों के साथ काम किया जा रहा है)
imageToPDF.selectText.4=एक ही पीडीएफ में मर्ज करें imageToPDF.selectText.4=एक ही पीडीएफ में मर्ज करें
imageToPDF.selectText.5=अलग-अलग पीडीएफ में परिवर्तित करें imageToPDF.selectText.5=अलग-अलग पीडीएफ में परिवर्तित करें
#pdfToImage #pdfToImage
pdfToImage.title=पीडीएफ से छवि pdfToImage.title=पीडीएफ से छवि
pdfToImage.header=पीडीएफ से छवि pdfToImage.header=पीडीएफ से छवि
@ -789,7 +790,7 @@ removePassword.submit=हटाएं
#changeMetadata #changeMetadata
changeMetadata.title=मेटाडेटा बदलें changeMetadata.title=शीर्षक:
changeMetadata.header=मेटाडेटा बदलें changeMetadata.header=मेटाडेटा बदलें
changeMetadata.selectText.1=कृपया उन चरों को संपादित करें जिन्हें आप बदलना चाहते हैं changeMetadata.selectText.1=कृपया उन चरों को संपादित करें जिन्हें आप बदलना चाहते हैं
changeMetadata.selectText.2=सभी मेटाडेटा हटाएं changeMetadata.selectText.2=सभी मेटाडेटा हटाएं

View File

@ -1,7 +1,7 @@
########### ###########
# Generic # # Generic #
########### ###########
# the direction that the language is written (ltr = left to right, rtl = right to left) # the direction that the language is written (ltr=left to right, rtl = right to left)
language.direction=ltr language.direction=ltr
pdfPrompt=Válasszon PDF-fájlokat pdfPrompt=Válasszon PDF-fájlokat
@ -19,7 +19,7 @@ save=Mentés
close=Bezárás close=Bezárás
filesSelected=kiválasztott fájlok filesSelected=kiválasztott fájlok
noFavourites=Nincs hozzáadva kedvenc noFavourites=Nincs hozzáadva kedvenc
bored=Unatkozol? bored=Unatkozol?
alphabet=Ábécé alphabet=Ábécé
downloadPdf=PDF letöltése downloadPdf=PDF letöltése
text=Szöveg text=Szöveg
@ -50,6 +50,7 @@ incorrectPasswordMessage=A jelenlegi jelszó helytelen.
usernameExistsMessage=Az új felhasználónév már létezik. usernameExistsMessage=Az új felhasználónév már létezik.
############# #############
# NAVBAR # # NAVBAR #
############# #############
@ -60,7 +61,6 @@ navbar.darkmode=Sötét mód
navbar.pageOps=Lap műveletek navbar.pageOps=Lap műveletek
navbar.settings=Beállítások navbar.settings=Beállítások
############# #############
# SETTINGS # # SETTINGS #
############# #############
@ -76,6 +76,7 @@ settings.signOut=Kijelentkezés
settings.accountSettings=Fiókbeállítások settings.accountSettings=Fiókbeállítások
changeCreds.title=Hitelesítés megváltoztatása changeCreds.title=Hitelesítés megváltoztatása
changeCreds.header=Frissítse fiókadatait changeCreds.header=Frissítse fiókadatait
changeCreds.changeUserAndPassword=Alapértelmezett bejelentkezési adatokat használ. Adjon meg egy új jelszót (és felhasználónevet, ha szeretné) changeCreds.changeUserAndPassword=Alapértelmezett bejelentkezési adatokat használ. Adjon meg egy új jelszót (és felhasználónevet, ha szeretné)
@ -86,11 +87,13 @@ changeCreds.confirmNewPassword=Új jelszó megerősítése
changeCreds.submit=Változtatások elküldése changeCreds.submit=Változtatások elküldése
account.title=Fiókbeállítások account.title=Fiókbeállítások
account.accountSettings=Fiókbeállítások account.accountSettings=Fiókbeállítások
account.adminSettings=Admin Beállítások - Felhasználók megtekintése és hozzáadása account.adminSettings=Admin Beállítások - Felhasználók megtekintése és hozzáadása
account.userControlSettings=Felhasználói vezérlési beállítások account.userControlSettings=Felhasználói vezérlési beállítások
account.changeUsername=Új felhasználónév account.changeUsername=Új felhasználónév
account.changeUsername=Új felhasználónév
account.password=Megerősítő jelszó account.password=Megerősítő jelszó
account.oldPassword=Régi jelszó account.oldPassword=Régi jelszó
account.newPassword=Új jelszó account.newPassword=Új jelszó
@ -116,16 +119,17 @@ adminUserSettings.role=Szerep
adminUserSettings.actions=Műveletek adminUserSettings.actions=Műveletek
adminUserSettings.apiUser=Korlátozott API-felhasználó adminUserSettings.apiUser=Korlátozott API-felhasználó
adminUserSettings.webOnlyUser=Csak webes felhasználó adminUserSettings.webOnlyUser=Csak webes felhasználó
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Kényszerítse a felhasználót a felhasználónév/jelszó megváltoztatására bejelentkezéskor adminUserSettings.forceChange=Kényszerítse a felhasználót a felhasználónév/jelszó megváltoztatására bejelentkezéskor
adminUserSettings.submit=Felhasználó mentése adminUserSettings.submit=Felhasználó mentése
############# #############
# HOME-PAGE # # HOME-PAGE #
############# #############
home.desc=Lokálisan hostolt egyszerű megoldás minden PDF igényéhez. home.desc=Lokálisan hostolt egyszerű megoldás minden PDF igényéhez.
home.searchBar=Keresés funkciókra... home.searchBar=Keresés funkciókra...
home.viewPdf.title=PDF Megtekintése home.viewPdf.title=PDF Megtekintése
home.viewPdf.desc=Megtekintés, annotálás, szöveg vagy képek hozzáadása home.viewPdf.desc=Megtekintés, annotálás, szöveg vagy képek hozzáadása
viewPdf.tags=megtekintés,olvasás,annotálás,szöveg,kép viewPdf.tags=megtekintés,olvasás,annotálás,szöveg,kép
@ -140,12 +144,13 @@ merge.tags=egyesítés,Lapműveletek,Háttér,server oldal
home.split.title=Osztás home.split.title=Osztás
home.split.desc=PDF-ek felosztása több dokumentumra home.split.desc=PDF-ek felosztása több dokumentumra
split.tags=Lapműveletek,osztás,Több oldal,vágás,server oldal split.tags=Lapműveletek,osztás,Több oldal,vágás,server oldal
home.rotate.title=Forgatás home.rotate.title=Forgatás
home.rotate.desc=PDF-ek egyszerű forgatása. home.rotate.desc=PDF-ek egyszerű forgatása.
rotate.tags=server oldal rotate.tags=server oldal
home.imageToPdf.title=Kép PDF-be home.imageToPdf.title=Kép PDF-be
home.imageToPdf.desc=Kép (PNG, JPEG, GIF) konvertálása PDF-fé. home.imageToPdf.desc=Kép (PNG, JPEG, GIF) konvertálása PDF-fé.
imageToPdf.tags=konverzió,img,jpg,kép,fotó imageToPdf.tags=konverzió,img,jpg,kép,fotó
@ -158,6 +163,7 @@ home.pdfOrganiser.title=Szervezés
home.pdfOrganiser.desc=Lapok eltávolítása/átszervezése bármilyen sorrendben home.pdfOrganiser.desc=Lapok eltávolítása/átszervezése bármilyen sorrendben
pdfOrganiser.tags=duplex,páros,páratlan,rendezés,mozgatás pdfOrganiser.tags=duplex,páros,páratlan,rendezés,mozgatás
home.addImage.title=Kép hozzáadása home.addImage.title=Kép hozzáadása
home.addImage.desc=Kép hozzáadása a PDF megadott helyére home.addImage.desc=Kép hozzáadása a PDF megadott helyére
addImage.tags=img,jpg,kép,fotó addImage.tags=img,jpg,kép,fotó
@ -170,6 +176,7 @@ home.permissions.title=Engedélyek módosítása
home.permissions.desc=Változtassa meg a PDF dokumentum engedélyeit home.permissions.desc=Változtassa meg a PDF dokumentum engedélyeit
permissions.tags=olvasás,írás,szerkesztés,nyomtatás permissions.tags=olvasás,írás,szerkesztés,nyomtatás
home.removePages.title=Eltávolítás home.removePages.title=Eltávolítás
home.removePages.desc=Szükségtelen lapok törlése a PDF dokumentumból. home.removePages.desc=Szükségtelen lapok törlése a PDF dokumentumból.
removePages.tags=Lapok eltávolítása,lapok törlése removePages.tags=Lapok eltávolítása,lapok törlése
@ -186,6 +193,7 @@ home.compressPdfs.title=Tömörítés
home.compressPdfs.desc=PDF-ek tömörítése a fájlméret csökkentése érdekében. home.compressPdfs.desc=PDF-ek tömörítése a fájlméret csökkentése érdekében.
compressPdfs.tags=szorít,kicsi,miniatűr compressPdfs.tags=szorít,kicsi,miniatűr
home.changeMetadata.title=Metaadatok Módosítása home.changeMetadata.title=Metaadatok Módosítása
home.changeMetadata.desc=Metaadatok Módosítása/Eltávolítása/Hozzáadása egy PDF dokumentumból home.changeMetadata.desc=Metaadatok Módosítása/Eltávolítása/Hozzáadása egy PDF dokumentumból
changeMetadata.tags=Cím,szerző,dátum,alkotás,idő,közzétevő,gyártó,statisztika changeMetadata.tags=Cím,szerző,dátum,alkotás,idő,közzétevő,gyártó,statisztika
@ -198,6 +206,7 @@ home.ocr.title=OCR / Tisztítás szkennelésekből
home.ocr.desc=Tisztítás szkennelésekből és szöveg észlelése képeken belül egy PDF-ben, majd visszahozza szövegként. home.ocr.desc=Tisztítás szkennelésekből és szöveg észlelése képeken belül egy PDF-ben, majd visszahozza szövegként.
ocr.tags=felismerés,szöveg,kép,szken,gép,felismert,azonosítás,szerkeszthető ocr.tags=felismerés,szöveg,kép,szken,gép,felismert,azonosítás,szerkeszthető
home.extractImages.title=Képek kinyerése home.extractImages.title=Képek kinyerése
home.extractImages.desc=Az összes kép kinyerése egy PDF-ből és mentése zip-be home.extractImages.desc=Az összes kép kinyerése egy PDF-ből és mentése zip-be
extractImages.tags=kép,fotó,mentés,archívum,zip,rögzítés,gyűjtés extractImages.tags=kép,fotó,mentés,archívum,zip,rögzítés,gyűjtés
@ -222,6 +231,7 @@ home.PDFToHTML.title=PDF >> HTML
home.PDFToHTML.desc=PDF konvertálása HTML formátumra home.PDFToHTML.desc=PDF konvertálása HTML formátumra
PDFToHTML.tags=web tartalom,böngészőbarát PDFToHTML.tags=web tartalom,böngészőbarát
home.PDFToXML.title=PDF >> XML home.PDFToXML.title=PDF >> XML
home.PDFToXML.desc=PDF konvertálása XML formátumra home.PDFToXML.desc=PDF konvertálása XML formátumra
PDFToXML.tags=adat-kinyerés,strukturált tartalom,interop,konverzió PDFToXML.tags=adat-kinyerés,strukturált tartalom,interop,konverzió
@ -246,6 +256,10 @@ home.removeBlanks.title=Üres lapok eltávolítása
home.removeBlanks.desc=Felismeri és eltávolítja az üres lapokat a dokumentumból home.removeBlanks.desc=Felismeri és eltávolítja az üres lapokat a dokumentumból
removeBlanks.tags=takarítás,egyszerűsítés,nem-tartalom,szervez removeBlanks.tags=takarítás,egyszerűsítés,nem-tartalom,szervez
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Összehasonlítás home.compare.title=Összehasonlítás
home.compare.desc=Összehasonlítja és megmutatja a különbségeket két PDF dokumentum között home.compare.desc=Összehasonlítja és megmutatja a különbségeket két PDF dokumentum között
compare.tags=kiemel,ellentét,változások,elemzés compare.tags=kiemel,ellentét,változások,elemzés
@ -271,7 +285,7 @@ home.add-page-numbers.desc=Lapszám hozzáadása a dokumentumhoz egy meghatároz
add-page-numbers.tags=lapszámozás,címke,szervez,index add-page-numbers.tags=lapszámozás,címke,szervez,index
home.auto-rename.title=Automatikus átnevezés PDF fájl home.auto-rename.title=Automatikus átnevezés PDF fájl
home.auto-rename.desc=Automatikusan átnevezi a PDF fájlt a felderített fejléc alapján home.auto-rename.desc=Automatikusan átnevezi a PDF fájlt a felderített fejléc alapján
auto-rename.tags=auto-felismerés,fejléc-alapú,szervezés,címkézés auto-rename.tags=auto-felismerés,fejléc-alapú,szervezés,címkézés
home.adjust-contrast.title=Színek/Kontraszt beállítása home.adjust-contrast.title=Színek/Kontraszt beállítása
@ -298,25 +312,30 @@ home.HTMLToPDF.title=HTML PDF-be
home.HTMLToPDF.desc=Bármely HTML fájl vagy tömörített fájl átalakítása PDF-be home.HTMLToPDF.desc=Bármely HTML fájl vagy tömörített fájl átalakítása PDF-be
HTMLToPDF.tags=markup,web-tartalom,transzformáció,konverzió HTMLToPDF.tags=markup,web-tartalom,transzformáció,konverzió
home.MarkdownToPDF.title=Markdown PDF-be home.MarkdownToPDF.title=Markdown PDF-be
home.MarkdownToPDF.desc=Bármely Markdown fájl átalakítása PDF-be home.MarkdownToPDF.desc=Bármely Markdown fájl átalakítása PDF-be
MarkdownToPDF.tags=markup,web-tartalom,transzformáció,konverzió MarkdownToPDF.tags=markup,web-tartalom,transzformáció,konverzió
home.getPdfInfo.title=Összes információ a PDF-ről home.getPdfInfo.title=Összes információ a PDF-ről
home.getPdfInfo.desc=Az összes lehetséges információ beszerzése a PDF-ekről home.getPdfInfo.desc=Az összes lehetséges információ beszerzése a PDF-ekről
getPdfInfo.tags=információ,adat,statisztika,statisztika getPdfInfo.tags=információ,adat,statisztika,statisztika
home.extractPage.title=Lapok kinyerése home.extractPage.title=Lapok kinyerése
home.extractPage.desc=Válassza ki a lapokat a PDF-ből home.extractPage.desc=Válassza ki a lapokat a PDF-ből
extractPage.tags=kinyer extractPage.tags=kinyer
home.PdfToSinglePage.title=PDF egyetlen nagy lapba home.PdfToSinglePage.title=PDF egyetlen nagy lapba
home.PdfToSinglePage.desc=Az összes PDF lap egyesítése egyetlen nagy lapba home.PdfToSinglePage.desc=Az összes PDF lap egyesítése egyetlen nagy lapba
PdfToSinglePage.tags=egyetlen lap PdfToSinglePage.tags=egyetlen lap
home.showJS.title=JavaScript megjelenítése home.showJS.title=JavaScript megjelenítése
home.showJS.desc=Keres és megjelenít bármilyen JS-t, amit beinjektáltak a PDF-be home.showJS.desc=Keres és megjelenít bármilyen JS-t, amit beinjektáltak a PDF-be
showJS.tags=JS showJS.tags=Elrejt,Elrejtés,kitakarás,fekete,fekete,marker,elrejtett
home.autoRedact.title=Automatikus Elrejtés home.autoRedact.title=Automatikus Elrejtés
home.autoRedact.desc=Automatikusan kitakar (elrejt) szöveget egy PDF-ben az input szöveg alapján home.autoRedact.desc=Automatikusan kitakar (elrejt) szöveget egy PDF-ben az input szöveg alapján
@ -326,10 +345,12 @@ home.tableExtraxt.title=PDF to CSV
home.tableExtraxt.desc=Táblázatok kinyerése a PDF-ből CSV formátumra konvertálva home.tableExtraxt.desc=Táblázatok kinyerése a PDF-ből CSV formátumra konvertálva
tableExtraxt.tags=CSV,Táblázat kinyerése,kinyer,konvertál tableExtraxt.tags=CSV,Táblázat kinyerése,kinyer,konvertál
home.autoSizeSplitPDF.title=Automatikus szétválasztás méret/számláló alapján home.autoSizeSplitPDF.title=Automatikus szétválasztás méret/számláló alapján
home.autoSizeSplitPDF.desc=Egyetlen PDF szétválasztása több dokumentummá méret, oldalszám vagy dokumentum szám alapján home.autoSizeSplitPDF.desc=Egyetlen PDF szétválasztása több dokumentummá méret, oldalszám vagy dokumentum szám alapján
autoSizeSplitPDF.tags=pdf,szétválasztás,dokumentum,szervezet autoSizeSplitPDF.tags=pdf,szétválasztás,dokumentum,szervezet
home.overlay-pdfs.title=PDF fájlok átlapolása home.overlay-pdfs.title=PDF fájlok átlapolása
home.overlay-pdfs.desc=PDF fájlok átlapolása egyik dokumentum a másik fölé helyezésével home.overlay-pdfs.desc=PDF fájlok átlapolása egyik dokumentum a másik fölé helyezésével
overlay-pdfs.tags=Átlapolás overlay-pdfs.tags=Átlapolás
@ -338,7 +359,6 @@ home.split-by-sections.title=PDF Szakaszokra osztása
home.split-by-sections.desc=Minden oldal felosztása kisebb vízszintes és függőleges szakaszokra home.split-by-sections.desc=Minden oldal felosztása kisebb vízszintes és függőleges szakaszokra
split-by-sections.tags=Szakasz elosztás, felosztás, testreszabás split-by-sections.tags=Szakasz elosztás, felosztás, testreszabás
########################### ###########################
# # # #
# WEB PAGES # # WEB PAGES #
@ -400,6 +420,7 @@ MarkdownToPDF.help=Az átalakítás folyamatban
MarkdownToPDF.credit=WeasyPrint alkalmazása MarkdownToPDF.credit=WeasyPrint alkalmazása
#url-to-pdf #url-to-pdf
URLToPDF.title=URL >> PDF URLToPDF.title=URL >> PDF
URLToPDF.header=URL >> PDF URLToPDF.header=URL >> PDF
@ -523,6 +544,12 @@ removeBlanks.whitePercentDesc=Az oldalakon található 'fehér' pixelek százal
removeBlanks.submit=Üres oldalak eltávolítása removeBlanks.submit=Üres oldalak eltávolítása
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Összehasonlítás compare.title=Összehasonlítás
compare.header=PDF-ek összehasonlítása compare.header=PDF-ek összehasonlítása
@ -565,7 +592,7 @@ ScannerImageSplit.selectText.8=A fotók minimális kontúrterületének beállí
ScannerImageSplit.selectText.9=Keret mérete: ScannerImageSplit.selectText.9=Keret mérete:
ScannerImageSplit.selectText.10=A hozzáadott és eltávolított keret méretének beállítása a fehér keretek elkerülése érdekében a kimeneten (alapértelmezett: 1). ScannerImageSplit.selectText.10=A hozzáadott és eltávolított keret méretének beállítása a fehér keretek elkerülése érdekében a kimeneten (alapértelmezett: 1).
#OCR #OCR
ocr.title=OCR / szkennelés tisztázása ocr.title=OCR / szkennelés tisztázása
ocr.header=Szkennelés tisztázása / OCR (Optikai karakterfelismerés) ocr.header=Szkennelés tisztázása / OCR (Optikai karakterfelismerés)
@ -639,12 +666,10 @@ pdfOrganiser.submit=Oldalak átrendezése
multiTool.title=PDF többfunkciós eszköz multiTool.title=PDF többfunkciós eszköz
multiTool.header=PDF többfunkciós eszköz multiTool.header=PDF többfunkciós eszköz
#view pdf #view pdf
viewPdf.title=PDF megtekintése viewPdf.title=PDF megtekintése
viewPdf.header=PDF megtekintése viewPdf.header=PDF megtekintése
#pageRemover #pageRemover
pageRemover.title=Oldaltörlő pageRemover.title=Oldaltörlő
pageRemover.header=PDF oldaltörlő pageRemover.header=PDF oldaltörlő
@ -686,8 +711,8 @@ imageToPDF.selectText.2=Automatikus forgatás PDF
imageToPDF.selectText.3=Több fájl logika (csak akkor engedélyezett, ha több képpel dolgozik) imageToPDF.selectText.3=Több fájl logika (csak akkor engedélyezett, ha több képpel dolgozik)
imageToPDF.selectText.4=Egyesítse egyetlen PDF-fé imageToPDF.selectText.4=Egyesítse egyetlen PDF-fé
imageToPDF.selectText.5=Átalakítás különálló PDF-fé imageToPDF.selectText.5=Átalakítás különálló PDF-fé
#pdfToImage #pdfToImage
pdfToImage.title=PDF képpé alakítása pdfToImage.title=PDF képpé alakítása
pdfToImage.header=PDF képpé alakítása pdfToImage.header=PDF képpé alakítása
@ -765,7 +790,7 @@ removePassword.submit=Eltávolítás
#changeMetadata #changeMetadata
changeMetadata.title=Metaadatok módosítása changeMetadata.title=Cím:
changeMetadata.header=Metaadatok módosítása changeMetadata.header=Metaadatok módosítása
changeMetadata.selectText.1=Kérjük, szerkessze azokat a változókat, amelyeket módosítani szeretne changeMetadata.selectText.1=Kérjük, szerkessze azokat a változókat, amelyeket módosítani szeretne
changeMetadata.selectText.2=Minden metaadat törlése changeMetadata.selectText.2=Minden metaadat törlése
@ -828,14 +853,12 @@ PDFToXML.header=PDF >> XML
PDFToXML.credit=Ez a szolgáltatás a LibreOffice-t használja a fájlkonverzióhoz. PDFToXML.credit=Ez a szolgáltatás a LibreOffice-t használja a fájlkonverzióhoz.
PDFToXML.submit=Konvertálás PDFToXML.submit=Konvertálás
#PDFToCSV #PDFToCSV
PDFToCSV.title=PDF >> CSV PDFToCSV.title=PDF >> CSV
PDFToCSV.header=PDF >> CSV PDFToCSV.header=PDF >> CSV
PDFToCSV.prompt=Válassza ki az oldalt a táblázat kinyeréséhez PDFToCSV.prompt=Válassza ki az oldalt a táblázat kinyeréséhez
PDFToCSV.submit=Kinyerés PDFToCSV.submit=Kinyerés
#split-by-size-or-count #split-by-size-or-count
split-by-size-or-count.header=PDF felosztása méret vagy oldalszám alapján split-by-size-or-count.header=PDF felosztása méret vagy oldalszám alapján
split-by-size-or-count.type.label=Válassza ki a felosztás típusát split-by-size-or-count.type.label=Válassza ki a felosztás típusát
@ -871,4 +894,3 @@ split-by-sections.vertical.label=Vízszintes szakaszok
split-by-sections.horizontal.placeholder=Adja meg a vízszintes szakaszok számát split-by-sections.horizontal.placeholder=Adja meg a vízszintes szakaszok számát
split-by-sections.vertical.placeholder=Adja meg a függőleges szakaszok számát split-by-sections.vertical.placeholder=Adja meg a függőleges szakaszok számát
split-by-sections.submit=Felosztás split-by-sections.submit=Felosztás

View File

@ -25,7 +25,7 @@ downloadPdf=Unduh PDF
text=Teks text=Teks
font=Jenis huruf font=Jenis huruf
selectFillter=-- Pilih -- selectFillter=-- Pilih --
pageNum=Nomor Halaman pageNum=Nomor Halaman
sizes.small=Kecil sizes.small=Kecil
sizes.medium=Sedang sizes.medium=Sedang
sizes.large=Besar sizes.large=Besar
@ -92,7 +92,7 @@ account.title=Pengaturan Akun
account.accountSettings=Pengaturan Akun account.accountSettings=Pengaturan Akun
account.adminSettings=Pengaturan Admin - Melihat dan Menambahkan Pengguna account.adminSettings=Pengaturan Admin - Melihat dan Menambahkan Pengguna
account.userControlSettings=Pengaturan Kontrol Pengguna account.userControlSettings=Pengaturan Kontrol Pengguna
account.changeUsername=Nama Pengguna Baru account.changeUsername=Ubah Nama Pengguna
account.changeUsername=Ubah Nama Pengguna account.changeUsername=Ubah Nama Pengguna
account.password=Konfirmasi Kata sandi account.password=Konfirmasi Kata sandi
account.oldPassword=Kata sandi lama account.oldPassword=Kata sandi lama
@ -119,6 +119,7 @@ adminUserSettings.role=Peran
adminUserSettings.actions=Tindakan adminUserSettings.actions=Tindakan
adminUserSettings.apiUser=Pengguna API Terbatas adminUserSettings.apiUser=Pengguna API Terbatas
adminUserSettings.webOnlyUser=Pengguna Khusus Web adminUserSettings.webOnlyUser=Pengguna Khusus Web
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Memaksa pengguna untuk mengubah nama pengguna/kata sandi saat masuk adminUserSettings.forceChange=Memaksa pengguna untuk mengubah nama pengguna/kata sandi saat masuk
adminUserSettings.submit=Simpan Pengguna adminUserSettings.submit=Simpan Pengguna
@ -143,7 +144,7 @@ merge.tags=menggabungkan,Pengoperasian halaman,Back end,sisi server
home.split.title=Membagi home.split.title=Membagi
home.split.desc=Membagi PDF menjadi beberapa dokumen home.split.desc=Membagi PDF menjadi beberapa dokumen
split.tags=Pengoperasian halaman,membagi,Multi Halaman,memotong,sisi server split.tags=Pengoperasian halaman,membagi,Multi Halaman,memotong,sisi server
home.rotate.title=Putar home.rotate.title=Putar
home.rotate.desc=Memutar PDF Anda dengan mudah. home.rotate.desc=Memutar PDF Anda dengan mudah.
@ -331,9 +332,10 @@ home.PdfToSinglePage.title=PDF ke Satu Halaman Besar
home.PdfToSinglePage.desc=Menggabungkan semua halaman PDF menjadi satu halaman besar home.PdfToSinglePage.desc=Menggabungkan semua halaman PDF menjadi satu halaman besar
PdfToSinglePage.tags=halaman tunggal PdfToSinglePage.tags=halaman tunggal
home.showJS.title=Tampilkan Javascript home.showJS.title=Tampilkan Javascript
home.showJS.desc=Mencari dan menampilkan JS apa pun yang disuntikkan ke dalam PDF home.showJS.desc=Mencari dan menampilkan JS apa pun yang disuntikkan ke dalam PDF
showJS.tags=JS showJS.tags=Hapus, Sembunyikan, padamkan, hitam, hitam, penanda, tersembunyi
home.autoRedact.title=Redaksional Otomatis home.autoRedact.title=Redaksional Otomatis
home.autoRedact.desc=Menyunting Otomatis (Menghitamkan) teks dalam PDF berdasarkan teks masukan home.autoRedact.desc=Menyunting Otomatis (Menghitamkan) teks dalam PDF berdasarkan teks masukan
@ -409,6 +411,7 @@ getPdfInfo.header=Dapatkan Info tentang PDF
getPdfInfo.submit=Dapatkan Info getPdfInfo.submit=Dapatkan Info
getPdfInfo.downloadJson=Unduh JSON getPdfInfo.downloadJson=Unduh JSON
#markdown-to-pdf #markdown-to-pdf
MarkdownToPDF.title=Markdown ke PDF MarkdownToPDF.title=Markdown ke PDF
MarkdownToPDF.header=Markdown Ke PDF MarkdownToPDF.header=Markdown Ke PDF
@ -417,6 +420,7 @@ MarkdownToPDF.help=Pekerjaan sedang berlangsung
MarkdownToPDF.credit=Menggunakan WeasyPrint MarkdownToPDF.credit=Menggunakan WeasyPrint
#url-to-pdf #url-to-pdf
URLToPDF.title=URL ke PDF URLToPDF.title=URL ke PDF
URLToPDF.header=URL Ke PDF URLToPDF.header=URL Ke PDF
@ -517,22 +521,22 @@ scalePages.submit=Kirim
#certSign #certSign
certSign.title=Penandatanganan Sertifikat certSign.title=Penandatanganan Sertifikat
certSign.header=Menandatangani PDF dengan sertifikat Anda (Sedang dalam proses) certSign.header=Menandatangani PDF dengan sertifikat Anda (Sedang dalam proses)
certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan: certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan:
certSign.selectKey=Pilih Berkas Kunci Pribadi Anda (format PKCS # 8, bisa .pem atau .der): certSign.selectKey=Pilih Berkas Kunci Pribadi Anda (format PKCS # 8, bisa .pem atau .der):
certSign.selectCert=Pilih Berkas Sertifikat Anda (format X.509, bisa .pem atau .der): certSign.selectCert=Pilih Berkas Sertifikat Anda (format X.509, bisa .pem atau .der):
certSign.selectP12=Pilih Berkas Keystore PKCS #12 Anda (.p12 atau .pfx) (Opsional, Jika disediakan, berkas tersebut harus berisi kunci pribadi dan sertifikat Anda): certSign.selectP12=Pilih Berkas Keystore PKCS #12 Anda (.p12 atau .pfx) (Opsional, Jika disediakan, berkas tersebut harus berisi kunci pribadi dan sertifikat Anda):
certSign.certType=Jenis Sertifikat certSign.certType=Jenis Sertifikat
certSign.password=Masukkan Kata Sandi Kunci atau Kunci Pribadi Anda (Jika Ada): certSign.password=Masukkan Kata Sandi Kunci atau Kunci Pribadi Anda (Jika Ada):
certSign.showSig=Tampilkan Tanda Tangan certSign.showSig=Tampilkan Tanda Tangan
certSign.reason=Alasan certSign.reason=Alasan
certSign.location=Lokasi certSign.location=Lokasi
certSign.name=Nama certSign.name=Nama
certSign.submit=Tanda tangani PDF certSign.submit=Tanda tangani PDF
#removeBlanks #removeBlanks
removeBlanks.title=Hapus Halaman Kosong removeBlanks.title=Hapus Halaman Kosong
hapusKosong.header=Hapus Halaman Kosong removeBlanks.header=Remove Blank Pages
removeBlanks.threshold=Ambang Batas Keputihan Piksel: removeBlanks.threshold=Ambang Batas Keputihan Piksel:
removeBlanks.thresholdDesc=Ambang batas untuk menentukan seberapa putih piksel putih yang harus diklasifikasikan sebagai 'Putih'. 0=Hitam, 255 putih murni. removeBlanks.thresholdDesc=Ambang batas untuk menentukan seberapa putih piksel putih yang harus diklasifikasikan sebagai 'Putih'. 0=Hitam, 255 putih murni.
removeBlanks.whitePercent=Persen Putih (%): removeBlanks.whitePercent=Persen Putih (%):
@ -553,6 +557,7 @@ compare.document.1=Dokumen 1
compare.document.2=Dokumen 2 compare.document.2=Dokumen 2
compare.submit=Bandingkan compare.submit=Bandingkan
#sign #sign
sign.title=Tanda sign.title=Tanda
sign.header=Tandatangani PDF sign.header=Tandatangani PDF
@ -607,6 +612,7 @@ ocr.help=Silakan baca dokumentasi ini tentang cara menggunakan ini untuk bahasa
ocr.credit=Layanan ini menggunakan OCRmyPDF dan Tesseract untuk OCR. ocr.credit=Layanan ini menggunakan OCRmyPDF dan Tesseract untuk OCR.
ocr.submit=Memproses PDF dengan OCR ocr.submit=Memproses PDF dengan OCR
#extractImages #extractImages
extractImages.title=Ekstrak Gambar extractImages.title=Ekstrak Gambar
extractImages.header=Mengekstrak Gambar extractImages.header=Mengekstrak Gambar
@ -616,7 +622,7 @@ extractImages.submit=Ekstrak
#File to PDF #File to PDF
fileToPDF.title=Berkas ke PDF fileToPDF.title=Berkas ke PDF
fileToPDF.header=Mengonversi berkas apa pun ke PDF fileToPDF.header=Mengonversi berkas apa pun ke PDF
fileToPDF.credit=Layanan ini menggunakan LibreOffice dan Unoconv untuk konversi berkas. fileToPDF.credit=Layanan ini menggunakan LibreOffice dan Unoconv untuk konversi berkas.
fileToPDF.supportedFileTypes=Jenis berkas yang didukung harus mencakup yang di bawah ini, namun untuk daftar lengkap format yang didukung, silakan lihat dokumentasi LibreOffice fileToPDF.supportedFileTypes=Jenis berkas yang didukung harus mencakup yang di bawah ini, namun untuk daftar lengkap format yang didukung, silakan lihat dokumentasi LibreOffice
fileToPDF.submit=Konversi ke PDF fileToPDF.submit=Konversi ke PDF
@ -630,7 +636,7 @@ compress.selectText.1=Mode Manual - Dari 1 hingga 4
compress.selectText.2=Tingkat Optimalisasi: compress.selectText.2=Tingkat Optimalisasi:
compress.selectText.3=4 (Buruk untuk gambar teks) compress.selectText.3=4 (Buruk untuk gambar teks)
compress.selectText.4=Mode Otomatis - Menyesuaikan kualitas secara otomatis untuk mendapatkan PDF dengan ukuran yang tepat compress.selectText.4=Mode Otomatis - Menyesuaikan kualitas secara otomatis untuk mendapatkan PDF dengan ukuran yang tepat
compress.selectText.5=Ukuran PDF yang diharapkan (mis. 25MB, 10,8MB, 25KB) compress.selectText.5=Ukuran PDF yang diharapkan (mis. 25MB, 10,8MB, 25KB)
compress.submit=Kompres compress.submit=Kompres
@ -750,7 +756,7 @@ watermark.selectText.1=Pilih PDF untuk menambahkan watermark:
watermark.selectText.2=Text Watermark: watermark.selectText.2=Text Watermark:
watermark.selectText.3=Ukuran Huruf: watermark.selectText.3=Ukuran Huruf:
watermark.selectText.4=Rotasi (0-360): watermark.selectText.4=Rotasi (0-360):
watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal): watermark.selectText.5=widthSpacer (Spasi diantara setiap watermark horisontal):
watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal): watermark.selectText.6=heightSpacer (Spasi diantara setiap watermark vertikal):
watermark.selectText.7=Opacity (0% - 100%): watermark.selectText.7=Opacity (0% - 100%):
watermark.selectText.8=Tipe Watermark: watermark.selectText.8=Tipe Watermark:
@ -784,7 +790,7 @@ removePassword.submit=Hapus
#changeMetadata #changeMetadata
changeMetadata.title=Ganti Metadata changeMetadata.title=Judul:
changeMetadata.header=Ganti Metadata changeMetadata.header=Ganti Metadata
changeMetadata.selectText.1=Silakan edit variabel yang ingin Anda ubah changeMetadata.selectText.1=Silakan edit variabel yang ingin Anda ubah
changeMetadata.selectText.2=Hapus semua metadata changeMetadata.selectText.2=Hapus semua metadata
@ -855,7 +861,7 @@ PDFToCSV.submit=Ektraksi
#split-by-size-or-count #split-by-size-or-count
split-by-size-or-count.header=Pisahkan PDF berdasarkan ukuran atau jumlah split-by-size-or-count.header=Pisahkan PDF berdasarkan ukuran atau jumlah
split-by-size-or-count.type.label= Pilih Tipe Split split-by-size-or-count.type.label=Pilih Tipe Split
split-by-size-or-count.type.size=Berdasarkan Ukuran split-by-size-or-count.type.size=Berdasarkan Ukuran
split-by-size-or-count.type.pageCount=Berdasarkan Jumlah Halaman split-by-size-or-count.type.pageCount=Berdasarkan Jumlah Halaman
split-by-size-or-count.type.docCount=Berdasarkan Jumlah Dokumen split-by-size-or-count.type.docCount=Berdasarkan Jumlah Dokumen

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Ruolo
adminUserSettings.actions=Azioni adminUserSettings.actions=Azioni
adminUserSettings.apiUser=Utente API limitato adminUserSettings.apiUser=Utente API limitato
adminUserSettings.webOnlyUser=Utente solo Web adminUserSettings.webOnlyUser=Utente solo Web
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Forza l'utente a cambiare nome username/password all'accesso adminUserSettings.forceChange=Forza l'utente a cambiare nome username/password all'accesso
adminUserSettings.submit=Salva utente adminUserSettings.submit=Salva utente
@ -255,6 +256,10 @@ home.removeBlanks.title=Rimuovi pagine vuote
home.removeBlanks.desc=Trova e rimuovi pagine vuote da un PDF. home.removeBlanks.desc=Trova e rimuovi pagine vuote da un PDF.
removeBlanks.tags=pulire,semplificare,non contenere contenuti,organizzare removeBlanks.tags=pulire,semplificare,non contenere contenuti,organizzare
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Compara home.compare.title=Compara
home.compare.desc=Vedi e compara le differenze tra due PDF. home.compare.desc=Vedi e compara le differenze tra due PDF.
compare.tags=differenziare,contrastare,cambiare,analisi compare.tags=differenziare,contrastare,cambiare,analisi
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentuale della pagina che deve essere bianca pe
removeBlanks.submit=Rimuovi removeBlanks.submit=Rimuovi
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Compara compare.title=Compara
compare.header=Compara PDF compare.header=Compara PDF

View File

@ -119,6 +119,7 @@ adminUserSettings.role=役割
adminUserSettings.actions=アクション adminUserSettings.actions=アクション
adminUserSettings.apiUser=限定されたAPIユーザー adminUserSettings.apiUser=限定されたAPIユーザー
adminUserSettings.webOnlyUser=ウェブ専用ユーザー adminUserSettings.webOnlyUser=ウェブ専用ユーザー
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する
adminUserSettings.submit=ユーザーの保存 adminUserSettings.submit=ユーザーの保存
@ -255,6 +256,10 @@ home.removeBlanks.title=空白ページの削除
home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。 home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=比較 home.compare.title=比較
home.compare.desc=2つのPDFを比較して表示します。 home.compare.desc=2つのPDFを比較して表示します。
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=削除するページの白の割合
removeBlanks.submit=空白ページの削除 removeBlanks.submit=空白ページの削除
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=比較 compare.title=比較
compare.header=PDFの比較 compare.header=PDFの比較

View File

@ -119,6 +119,7 @@ adminUserSettings.role=역할
adminUserSettings.actions=동작 adminUserSettings.actions=동작
adminUserSettings.apiUser=제한된 API 사용 adminUserSettings.apiUser=제한된 API 사용
adminUserSettings.webOnlyUser=웹 사용만 허용 adminUserSettings.webOnlyUser=웹 사용만 허용
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=다음 로그인 때 사용자명과 비밀번호를 변경하도록 강제 adminUserSettings.forceChange=다음 로그인 때 사용자명과 비밀번호를 변경하도록 강제
adminUserSettings.submit=사용자 저장 adminUserSettings.submit=사용자 저장
@ -255,6 +256,10 @@ home.removeBlanks.title=빈 페이지 제거
home.removeBlanks.desc=PDF 문서에서 빈 페이지를 감지하고 제거합니다. home.removeBlanks.desc=PDF 문서에서 빈 페이지를 감지하고 제거합니다.
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=비교 home.compare.title=비교
home.compare.desc=2개의 PDF 문서를 비교하고 차이를 표시합니다. home.compare.desc=2개의 PDF 문서를 비교하고 차이를 표시합니다.
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=제거될 페이지의 흰색 픽셀 비율
removeBlanks.submit=빈 페이지 제거 removeBlanks.submit=빈 페이지 제거
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=비교 compare.title=비교
compare.header=PDF 문서 비교 compare.header=PDF 문서 비교

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rol
adminUserSettings.actions=Acties adminUserSettings.actions=Acties
adminUserSettings.apiUser=Beperkte API gebruiker adminUserSettings.apiUser=Beperkte API gebruiker
adminUserSettings.webOnlyUser=Alleen web gebruiker adminUserSettings.webOnlyUser=Alleen web gebruiker
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Sla gebruiker op adminUserSettings.submit=Sla gebruiker op
@ -255,6 +256,10 @@ home.removeBlanks.title=Verwijder lege pagina''s
home.removeBlanks.desc=Detecteert en verwijdert lege pagina''s uit een document home.removeBlanks.desc=Detecteert en verwijdert lege pagina''s uit een document
removeBlanks.tags=opruimen,stroomlijnen,geen-inhoud,organiseren removeBlanks.tags=opruimen,stroomlijnen,geen-inhoud,organiseren
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Vergelijken home.compare.title=Vergelijken
home.compare.desc=Vergelijkt en toont de verschillen tussen 2 PDF-documenten home.compare.desc=Vergelijkt en toont de verschillen tussen 2 PDF-documenten
compare.tags=onderscheiden,contrasteren,veranderingen,analyse compare.tags=onderscheiden,contrasteren,veranderingen,analyse
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentage van de pagina dat ''witte'' pixels moet
removeBlanks.submit=Blanco''s verwijderen removeBlanks.submit=Blanco''s verwijderen
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Vergelijken compare.title=Vergelijken
compare.header=PDF''s vergelijken compare.header=PDF''s vergelijken

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
@ -255,6 +256,10 @@ home.removeBlanks.title=Usuń puste strony
home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF home.removeBlanks.desc=Wykrywa i usuwa puste strony z dokumentu PDF
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Porównaj home.compare.title=Porównaj
home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF home.compare.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Procent strony, która musi być biała, aby zosta
removeBlanks.submit=Usuń puste removeBlanks.submit=Usuń puste
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Porównaj compare.title=Porównaj
compare.header=Porównaj PDF(y) compare.header=Porównaj PDF(y)

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
@ -255,6 +256,10 @@ home.removeBlanks.title=Удалить пустые страницы
home.removeBlanks.desc=Обнаруживает и удаляет пустые страницы из документа home.removeBlanks.desc=Обнаруживает и удаляет пустые страницы из документа
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Сравнение home.compare.title=Сравнение
home.compare.desc=Сравнивает и показывает различия между двумя PDF-документами home.compare.desc=Сравнивает и показывает различия между двумя PDF-документами
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Общий процент белого на стр
removeBlanks.submit=Удалить Пустые removeBlanks.submit=Удалить Пустые
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Сравнение compare.title=Сравнение
compare.header=Сравнение PDFы compare.header=Сравнение PDFы

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Role
adminUserSettings.actions=Actions adminUserSettings.actions=Actions
adminUserSettings.apiUser=Limited API User adminUserSettings.apiUser=Limited API User
adminUserSettings.webOnlyUser=Web Only User adminUserSettings.webOnlyUser=Web Only User
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.forceChange=Force user to change username/password on login
adminUserSettings.submit=Save User adminUserSettings.submit=Save User
@ -255,6 +256,10 @@ home.removeBlanks.title=Ta bort tomma sidor
home.removeBlanks.desc=Känner av och tar bort tomma sidor från ett dokument home.removeBlanks.desc=Känner av och tar bort tomma sidor från ett dokument
removeBlanks.tags=cleanup,streamline,non-content,organize removeBlanks.tags=cleanup,streamline,non-content,organize
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Jämför home.compare.title=Jämför
home.compare.desc=Jämför och visar skillnaderna mellan 2 PDF-dokument home.compare.desc=Jämför och visar skillnaderna mellan 2 PDF-dokument
compare.tags=differentiate,contrast,changes,analysis compare.tags=differentiate,contrast,changes,analysis
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Procentandel av sidan som måste vara vit för att
removeBlanks.submit=Ta bort tomrum removeBlanks.submit=Ta bort tomrum
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Jämför compare.title=Jämför
compare.header=Jämför PDF-filer compare.header=Jämför PDF-filer

View File

@ -119,6 +119,7 @@ adminUserSettings.role=Rol
adminUserSettings.actions=Eylemler adminUserSettings.actions=Eylemler
adminUserSettings.apiUser=Sınırlı API Kullanıcısı adminUserSettings.apiUser=Sınırlı API Kullanıcısı
adminUserSettings.webOnlyUser=Sadece Web Kullanıcısı adminUserSettings.webOnlyUser=Sadece Web Kullanıcısı
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=Kullanıcının girişte kullanıcı adı/şifre değiştirmesini zorla adminUserSettings.forceChange=Kullanıcının girişte kullanıcı adı/şifre değiştirmesini zorla
adminUserSettings.submit=Kullanıcıyı Kaydet adminUserSettings.submit=Kullanıcıyı Kaydet
@ -255,6 +256,10 @@ home.removeBlanks.title=Boş Sayfaları Kaldır
home.removeBlanks.desc=Bir belgeden boş sayfaları tespit eder ve kaldırır home.removeBlanks.desc=Bir belgeden boş sayfaları tespit eder ve kaldırır
removeBlanks.tags=temizle,sadeleştir,içeriksiz,düzenle removeBlanks.tags=temizle,sadeleştir,içeriksiz,düzenle
home.removeAnnotations.title=Remove Annotations
home.removeAnnotations.desc=Removes all comments/annotations from a PDF
removeAnnotations.tags=comments,highlight,notes,markup,remove
home.compare.title=Karşılaştır home.compare.title=Karşılaştır
home.compare.desc=2 PDF Belgesi arasındaki farkları karşılaştırır ve gösterir home.compare.desc=2 PDF Belgesi arasındaki farkları karşılaştırır ve gösterir
compare.tags=farklılaştır,karşılaştır,değişiklikler,analiz compare.tags=farklılaştır,karşılaştır,değişiklikler,analiz
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Bir sayfanın 'beyaz' pixel olması gereken yüzde
removeBlanks.submit=Boşları Kaldır removeBlanks.submit=Boşları Kaldır
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=Karşılaştır compare.title=Karşılaştır
compare.header=PDF'leri Karşılaştır compare.header=PDF'leri Karşılaştır

View File

@ -119,6 +119,7 @@ adminUserSettings.role=角色
adminUserSettings.actions=操作 adminUserSettings.actions=操作
adminUserSettings.apiUser=有限 API 用户 adminUserSettings.apiUser=有限 API 用户
adminUserSettings.webOnlyUser=仅限 Web 用户 adminUserSettings.webOnlyUser=仅限 Web 用户
adminUserSettings.demoUser=Demo User (No custom settings)
adminUserSettings.forceChange=强制用户在登录时更改用户名/密码 adminUserSettings.forceChange=强制用户在登录时更改用户名/密码
adminUserSettings.submit=保存用户 adminUserSettings.submit=保存用户
@ -126,12 +127,12 @@ adminUserSettings.submit=保存用户
# HOME-PAGE # # HOME-PAGE #
############# #############
home.desc=CZL一站式服务满足您的所有PDF需求。 home.desc=CZL一站式服务满足您的所有PDF需求。
home.searchBar=Search for features... home.searchBar=搜索您需要的功能...
home.viewPdf.title=View PDF home.viewPdf.title=浏览PDF
home.viewPdf.desc=View, annotate, add text or images home.viewPdf.desc=浏览、注释、添加文本或图像
viewPdf.tags=view,read,annotate,text,image viewPdf.tags=浏览、阅读、注释、文本、图像
home.multiTool.title=PDF多功能工具 home.multiTool.title=PDF多功能工具
home.multiTool.desc=合并、旋转、重新排列和删除PDF页面 home.multiTool.desc=合并、旋转、重新排列和删除PDF页面
@ -255,6 +256,10 @@ home.removeBlanks.title=删除空白页
home.removeBlanks.desc=检测并删除文档中的空白页 home.removeBlanks.desc=检测并删除文档中的空白页
removeBlanks.tags=清理、简化、非内容、整理 removeBlanks.tags=清理、简化、非内容、整理
home.removeAnnotations.title=删除标注
home.removeAnnotations.desc=删除PDF中的所有标注/评论
removeAnnotations.tags=评论、高亮、笔记、标注、删除
home.compare.title=比较 home.compare.title=比较
home.compare.desc=比较并显示两个PDF文档之间的差异 home.compare.desc=比较并显示两个PDF文档之间的差异
compare.tags=区分、对比、更改、分析 compare.tags=区分、对比、更改、分析
@ -337,22 +342,22 @@ home.autoRedact.desc=根据输入文本自动删除覆盖PDF中的文本
showJS.tags=JavaScript showJS.tags=JavaScript
home.tableExtraxt.title=PDF to CSV home.tableExtraxt.title=PDF to CSV
home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV home.tableExtraxt.desc=从PDF中提取表格并将其转换为CSV
tableExtraxt.tags=CSV,Table Extraction,extract,convert tableExtraxt.tags=CSV、表格提取、提取、转换
home.autoSizeSplitPDF.title=Auto Split by Size/Count home.autoSizeSplitPDF.title=自动根据大小/数目拆分PDF
home.autoSizeSplitPDF.desc=Split a single PDF into multiple documents based on size, page count, or document count home.autoSizeSplitPDF.desc=将单个PDF拆分为多个文档基于大小、页数或文档数
autoSizeSplitPDF.tags=pdf,split,document,organization autoSizeSplitPDF.tags=pdf、拆分、文件、组织
home.overlay-pdfs.title=Overlay PDFs home.overlay-pdfs.title=叠加PDF
home.overlay-pdfs.desc=Overlays PDFs on-top of another PDF home.overlay-pdfs.desc=将PDF叠加在另一个PDF上
overlay-pdfs.tags=Overlay overlay-pdfs.tags=叠加
home.split-by-sections.title=Split PDF by Sections home.split-by-sections.title=拆分PDF成小块
home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections home.split-by-sections.desc=将PDF的每一页分割成更小的水平和垂直的部分
split-by-sections.tags=Section Split, Divide, Customize split-by-sections.tags=章节拆分、分割、自定义
########################### ###########################
# # # #
@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=必须为白色才能删除的页面百分比
removeBlanks.submit=删除空白 removeBlanks.submit=删除空白
#removeAnnotations
removeAnnotations.title=Remove Annotations
removeAnnotations.header=Remove Annotations
removeAnnotations.submit=Remove
#compare #compare
compare.title=比较 compare.title=比较
compare.header=比较 PDF compare.header=比较 PDF
@ -843,43 +854,43 @@ PDFToXML.credit=此服务使用LibreOffice进行文件转换。
PDFToXML.submit=转换 PDFToXML.submit=转换
#PDFToCSV #PDFToCSV
PDFToCSV.title=PDF ? CSV PDFToCSV.title=PDF To CSV
PDFToCSV.header=PDF ? CSV PDFToCSV.header=将 PDF 转换为 CSV
PDFToCSV.prompt=Choose page to extract table PDFToCSV.prompt=选择需要提取表格的页面
PDFToCSV.submit=?? PDFToCSV.submit=提取
#split-by-size-or-count #split-by-size-or-count
split-by-size-or-count.header=Split PDF by Size or Count split-by-size-or-count.header=按照大小或数目拆分PDF
split-by-size-or-count.type.label=Select Split Type split-by-size-or-count.type.label=选择拆分类型
split-by-size-or-count.type.size=By Size split-by-size-or-count.type.size=按照大小
split-by-size-or-count.type.pageCount=By Page Count split-by-size-or-count.type.pageCount=按照页数
split-by-size-or-count.type.docCount=By Document Count split-by-size-or-count.type.docCount=按照文档数
split-by-size-or-count.value.label=Enter Value split-by-size-or-count.value.label=输入数值
split-by-size-or-count.value.placeholder=Enter size (e.g., 2MB or 3KB) or count (e.g., 5) split-by-size-or-count.value.placeholder=输入大小例如2MB或3KB或数目例如5
split-by-size-or-count.submit=Submit split-by-size-or-count.submit=提交
#overlay-pdfs #overlay-pdfs
overlay-pdfs.header=Overlay PDF Files overlay-pdfs.header=叠加PDF文件
overlay-pdfs.baseFile.label=Select Base PDF File overlay-pdfs.baseFile.label=选择基础PDF文件
overlay-pdfs.overlayFiles.label=Select Overlay PDF Files overlay-pdfs.overlayFiles.label=选择需要叠加在基础上的PDF文件
overlay-pdfs.mode.label=Select Overlay Mode overlay-pdfs.mode.label=选择叠加模式
overlay-pdfs.mode.sequential=Sequential Overlay overlay-pdfs.mode.sequential=按顺序叠加
overlay-pdfs.mode.interleaved=Interleaved Overlay overlay-pdfs.mode.interleaved=交错叠加
overlay-pdfs.mode.fixedRepeat=Fixed Repeat Overlay overlay-pdfs.mode.fixedRepeat=固定重复叠加
overlay-pdfs.counts.label=Overlay Counts (for Fixed Repeat Mode) overlay-pdfs.counts.label=叠加次数(仅限固定重复叠加模式)
overlay-pdfs.counts.placeholder=Enter comma-separated counts (e.g., 2,3,1) overlay-pdfs.counts.placeholder=输入用逗号分隔的次数例如2,3,1
overlay-pdfs.position.label=Select Overlay Position overlay-pdfs.position.label=选择叠加位置
overlay-pdfs.position.foreground=Foreground overlay-pdfs.position.foreground=前面(上面)
overlay-pdfs.position.background=Background overlay-pdfs.position.background=后面(下面)
overlay-pdfs.submit=Submit overlay-pdfs.submit=提交
#split-by-sections #split-by-sections
split-by-sections.title=Split PDF by Sections split-by-sections.title=按照块Section拆分PDF
split-by-sections.header=Split PDF into Sections split-by-sections.header=将PDF拆分成块
split-by-sections.horizontal.label=Horizontal Divisions split-by-sections.horizontal.label=水平分割
split-by-sections.vertical.label=Vertical Divisions split-by-sections.vertical.label=垂直分割
split-by-sections.horizontal.placeholder=Enter number of horizontal divisions split-by-sections.horizontal.placeholder=输入水平分割数
split-by-sections.vertical.placeholder=Enter number of vertical divisions split-by-sections.vertical.placeholder=输入垂直分割数
split-by-sections.submit=Split PDF split-by-sections.submit=分割PDF

View File

@ -5,10 +5,13 @@
security: security:
enableLogin: false # set to 'true' to enable login enableLogin: false # set to 'true' to enable login
csrfDisabled: true csrfDisabled: true
loginAttemptCount: 5 # lock user account after 5 tries
loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts
system: system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes)
#ui: #ui:
# appName: exampleAppName # Application's visible name # appName: exampleAppName # Application's visible name

View File

@ -75,6 +75,13 @@ table th, table td {
border: none; border: none;
color: #fff !important; color: #fff !important;
} }
.btn-warning {
background-color: #ffc107 !important;
border: none;
color: #000 !important;
}
.btn-outline-secondary { .btn-outline-secondary {
color: #fff !important; color: #fff !important;
border-color: #fff; border-color: #fff;
@ -92,6 +99,11 @@ hr {
background-color: rgba(255, 255, 255, 0.6); /* for some browsers that might use background instead of border for <hr> */ background-color: rgba(255, 255, 255, 0.6); /* for some browsers that might use background instead of border for <hr> */
} }
.modal-content {
color: #fff !important;
border-color: #fff;
}
#global-buttons-container input { #global-buttons-container input {
background-color: #323948; background-color: #323948;
caret-color: #ffffff; caret-color: #ffffff;

View File

@ -1,5 +1,5 @@
#searchBar { #searchBar {
background-image: url('/images/search.svg'); background-image: url('../images/search.svg');
background-position: 16px 16px; background-position: 16px 16px;
background-repeat: no-repeat; background-repeat: no-repeat;
width: 100%; width: 100%;

View File

@ -12,20 +12,17 @@ function validatePipeline() {
if (currentOperation === '/add-password') { if (currentOperation === '/add-password') {
containsAddPassword = true; containsAddPassword = true;
} }
console.log(currentOperation);
console.log(apiDocs[currentOperation]);
let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; let currentOperationDescription = apiDocs[currentOperation]?.post?.description || "";
let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; let nextOperationDescription = apiDocs[nextOperation]?.post?.description || "";
console.log("currentOperationDescription", currentOperationDescription); // Strip off 'ZIP-' prefix
console.log("nextOperationDescription", nextOperationDescription); currentOperationDescription = currentOperationDescription.replace("ZIP-", '');
nextOperationDescription = nextOperationDescription.replace("ZIP-", '');
let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || ""; let currentOperationOutput = currentOperationDescription.match(/Output:([A-Z\/]*)/)?.[1] || "";
let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || ""; let nextOperationInput = nextOperationDescription.match(/Input:([A-Z\/]*)/)?.[1] || "";
console.log("Operation " + currentOperation + " Output: " + currentOperationOutput);
console.log("Operation " + nextOperation + " Input: " + nextOperationInput);
// Splitting in case of multiple possible output/input // Splitting in case of multiple possible output/input
let currentOperationOutputArr = currentOperationOutput.split('/'); let currentOperationOutputArr = currentOperationOutput.split('/');
let nextOperationInputArr = nextOperationInput.split('/'); let nextOperationInputArr = nextOperationInput.split('/');
@ -35,6 +32,7 @@ function validatePipeline() {
console.log(`Intersection: ${intersection}`); console.log(`Intersection: ${intersection}`);
if (intersection.length === 0) { if (intersection.length === 0) {
updateValidateButton(false);
isValid = false; isValid = false;
console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); console.log(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`); alert(`Incompatible operations: The output of operation '${currentOperation}' (${currentOperationOutput}) is not compatible with the input of the following operation '${nextOperation}' (${nextOperationInput}).`);
@ -43,6 +41,7 @@ function validatePipeline() {
} }
} }
if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') { if (containsAddPassword && pipelineListItems[pipelineListItems.length - 1].querySelector('.operationName').textContent !== '/add-password') {
updateValidateButton(false);
alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.');
return false; return false;
} }
@ -53,10 +52,20 @@ function validatePipeline() {
console.error('Pipeline is not valid'); console.error('Pipeline is not valid');
// Stop operation, maybe display an error to the user // Stop operation, maybe display an error to the user
} }
updateValidateButton(isValid);
return isValid; return isValid;
} }
function updateValidateButton(isValid) {
var validateButton = document.getElementById('validateButton');
if (isValid) {
validateButton.classList.remove('btn-danger');
validateButton.classList.add('btn-success');
} else {
validateButton.classList.remove('btn-success');
validateButton.classList.add('btn-danger');
}
}
@ -67,14 +76,14 @@ document.getElementById('submitConfigBtn').addEventListener('click', function()
return; return;
} }
let selectedOperation = document.getElementById('operationsDropdown').value; let selectedOperation = document.getElementById('operationsDropdown').value;
let parameters = operationSettings[selectedOperation] || {};
var pipelineName = document.getElementById('pipelineName').value;
let pipelineList = document.getElementById('pipelineList').children;
let pipelineConfig = { let pipelineConfig = {
"name": "uniquePipelineName", "name": pipelineName,
"pipeline": [{ "pipeline": [],
"operation": selectedOperation,
"parameters": parameters
}],
"_examples": { "_examples": {
"outputDir": "{outputFolder}/{folderName}", "outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}" "outputFileName": "{filename}-{pipelineName}-{date}-{time}"
@ -83,6 +92,28 @@ document.getElementById('submitConfigBtn').addEventListener('click', function()
"outputFileName": "{filename}" "outputFileName": "{filename}"
}; };
for (let i = 0; i < pipelineList.length; i++) {
let operationName = pipelineList[i].querySelector('.operationName').textContent;
let parameters = operationSettings[operationName] || {};
pipelineConfig.pipeline.push({
"operation": operationName,
"parameters": parameters
});
}
let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2); let pipelineConfigJson = JSON.stringify(pipelineConfig, null, 2);
let formData = new FormData(); let formData = new FormData();
@ -99,37 +130,50 @@ document.getElementById('submitConfigBtn').addEventListener('click', function()
formData.append('json', pipelineConfigJson); formData.append('json', pipelineConfigJson);
console.log("formData", formData); console.log("formData", formData);
fetch('/handleData', { fetch('api/v1/pipeline/handleData', {
method: 'POST', method: 'POST',
body: formData body: formData
}) })
.then(response => response.blob()) .then(response => {
.then(blob => { // Save the response to use it later
const responseToUseLater = response;
return response.blob().then(blob => {
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
// Use responseToUseLater instead of response
const contentDisposition = responseToUseLater.headers.get('Content-Disposition');
let filename = 'download';
if (contentDisposition && contentDisposition.indexOf('attachment') !== -1) {
filename = decodeURIComponent(contentDisposition.split('filename=')[1].replace(/"/g, '')).trim();
}
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
});
})
.catch((error) => {
console.error('Error:', error);
});
let url = window.URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = 'outputfile';
document.body.appendChild(a);
a.click();
a.remove();
})
.catch((error) => {
console.error('Error:', error);
});
}); });
let apiDocs = {}; let apiDocs = {};
let apiSchemas = {};
let operationSettings = {}; let operationSettings = {};
fetch('v3/api-docs') fetch('v1/api-docs')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
apiDocs = data.paths; apiDocs = data.paths;
apiSchemas = data.components.schemas;
let operationsDropdown = document.getElementById('operationsDropdown'); let operationsDropdown = document.getElementById('operationsDropdown');
const ignoreOperations = ["/handleData", "operationToIgnore"]; // Add the operations you want to ignore here const ignoreOperations = ["/api/v1/pipeline/handleData", "/api/v1/pipeline/operationToIgnore"]; // Add the operations you want to ignore here
operationsDropdown.innerHTML = ''; operationsDropdown.innerHTML = '';
@ -138,6 +182,9 @@ fetch('v3/api-docs')
// Group operations by tags // Group operations by tags
Object.keys(data.paths).forEach(operationPath => { Object.keys(data.paths).forEach(operationPath => {
let operation = data.paths[operationPath].post; let operation = data.paths[operationPath].post;
if(!operation || !operation.description) {
console.log(operationPath);
}
if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) { if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) {
let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag
if (!operationsByTag[operationTag]) { if (!operationsByTag[operationTag]) {
@ -146,9 +193,8 @@ fetch('v3/api-docs')
operationsByTag[operationTag].push(operationPath); operationsByTag[operationTag].push(operationPath);
} }
}); });
// Specify the order of tags // Specify the order of tags
let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"];
// Create dropdown options // Create dropdown options
tagOrder.forEach(tag => { tagOrder.forEach(tag => {
@ -158,8 +204,18 @@ fetch('v3/api-docs')
operationsByTag[tag].forEach(operationPath => { operationsByTag[tag].forEach(operationPath => {
let option = document.createElement('option'); let option = document.createElement('option');
let operationWithoutSlash = operationPath.replace(/\//g, ''); // Remove slashes
option.textContent = operationWithoutSlash; let operationPathDisplay = operationPath
operationPathDisplay = operationPath.replace(new RegExp("api/v1/" + tag.toLowerCase() + "/", 'i'), "");
if(operationPath.includes("/convert")){
operationPathDisplay = operationPathDisplay.replace(/^\//, '').replaceAll("/", " to ");
} else {
operationPathDisplay = operationPathDisplay.replace(/\//g, ''); // Remove slashes
}
operationPathDisplay = operationPathDisplay.replaceAll(" ","-");
option.textContent = operationPathDisplay;
option.value = operationPath; // Keep the value with slashes for querying option.value = operationPath; // Keep the value with slashes for querying
group.appendChild(option); group.appendChild(option);
}); });
@ -176,25 +232,40 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
let listItem = document.createElement('li'); let listItem = document.createElement('li');
listItem.className = "list-group-item"; listItem.className = "list-group-item";
let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && let hasSettings = false;
((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) {
(apiDocs[selectedOperation].post.requestBody && const postMethod = apiDocs[selectedOperation].post;
apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties)));
// Check if parameters exist
if (postMethod.parameters && postMethod.parameters.length > 0) {
hasSettings = true;
} else if (postMethod.requestBody && postMethod.requestBody.content['multipart/form-data']) {
// Extract the reference key
const refKey = postMethod.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop();
// Check if the referenced schema exists and has properties
if (apiSchemas[refKey] && Object.keys(apiSchemas[refKey].properties).length > 0) {
hasSettings = true;
}
}
}
listItem.innerHTML = ` listItem.innerHTML = `
<div class="d-flex justify-content-between align-items-center w-100"> <div class="d-flex justify-content-between align-items-center w-100">
<div class="operationName">${selectedOperation}</div> <div class="operationName">${selectedOperation}</div>
<div class="arrows d-flex"> <div class="arrows d-flex">
<button class="btn btn-secondary move-up btn-margin"><span>&uarr;</span></button> <button class="btn btn-secondary move-up ms-1"><span>&uarr;</span></button>
<button class="btn btn-secondary move-down btn-margin"><span>&darr;</span></button> <button class="btn btn-secondary move-down ms-1"><span>&darr;</span></button>
<button class="btn btn-warning pipelineSettings btn-margin" ${hasSettings ? "" : "disabled"}><span style="color: ${hasSettings ? "black" : "grey"};"></span></button> <button class="btn ${hasSettings ? 'btn-warning' : 'btn-secondary'} pipelineSettings ms-1" ${hasSettings ? "" : "disabled"}>
<button class="btn btn-danger remove"><span>X</span></button> <span style="color: ${hasSettings ? "white" : "grey"};"></span>
</div> </button>
</div> <button class="btn btn-danger remove ms-1"><span>X</span></button>
`; </div>
</div>
`;
pipelineList.appendChild(listItem); pipelineList.appendChild(listItem);
@ -215,23 +286,28 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
listItem.querySelector('.remove').addEventListener('click', function(event) { listItem.querySelector('.remove').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
pipelineList.removeChild(listItem); pipelineList.removeChild(listItem);
hideOrShowPipelineHeader();
}); });
listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) {
event.preventDefault(); event.preventDefault();
showpipelineSettingsModal(selectedOperation); showpipelineSettingsModal(selectedOperation);
hideOrShowPipelineHeader();
}); });
function showpipelineSettingsModal(operation) { function showpipelineSettingsModal(operation) {
let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsModal = document.getElementById('pipelineSettingsModal');
let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent');
let operationData = apiDocs[operation].post.parameters || []; let operationData = apiDocs[operation].post.parameters || [];
let requestBodyData = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema.properties || {};
// Resolve the $ref reference to get actual schema properties
let refKey = apiDocs[operation].post.requestBody.content['multipart/form-data'].schema['$ref'].split('/').pop();
let requestBodyData = apiSchemas[refKey].properties || {};
// Combine operationData and requestBodyData into a single array // Combine operationData and requestBodyData into a single array
operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({
name: key, name: key,
schema: requestBodyData[key] schema: requestBodyData[key]
}))); })));
pipelineSettingsContent.innerHTML = ''; pipelineSettingsContent.innerHTML = '';
@ -245,11 +321,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
let parameterLabel = document.createElement('label'); let parameterLabel = document.createElement('label');
parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `;
parameterLabel.title = parameter.description; parameterLabel.title = parameter.schema.description;
parameterLabel.setAttribute('for', parameter.name);
parameterDiv.appendChild(parameterLabel); parameterDiv.appendChild(parameterLabel);
let defaultValue = parameter.schema.example;
if (defaultValue === undefined) defaultValue = parameter.schema.default;
let parameterInput; let parameterInput;
// check if enum exists in schema // check if enum exists in schema
if (parameter.schema.enum) { if (parameter.schema.enum) {
// if enum exists, create a select element // if enum exists, create a select element
@ -277,11 +357,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'text'; parameterInput.type = 'text';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
parameterInput.value = "automatedFileInput"; parameterInput.value = "FileInputPathToBeInputtedManuallyOffline";
} else { } else {
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'text'; parameterInput.type = 'text';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
if (defaultValue !== undefined) parameterInput.value = defaultValue;
} }
break; break;
case 'number': case 'number':
@ -289,10 +370,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'number'; parameterInput.type = 'number';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
if (defaultValue !== undefined) parameterInput.value = defaultValue;
break; break;
case 'boolean': case 'boolean':
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'checkbox'; parameterInput.type = 'checkbox';
if (defaultValue === true) parameterInput.checked = true;
break; break;
case 'array': case 'array':
case 'object': case 'object':
@ -304,10 +387,13 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
parameterInput = document.createElement('input'); parameterInput = document.createElement('input');
parameterInput.type = 'text'; parameterInput.type = 'text';
parameterInput.className = "form-control"; parameterInput.className = "form-control";
if (defaultValue !== undefined) parameterInput.value = defaultValue;
} }
} }
parameterInput.id = parameter.name; parameterInput.id = parameter.name;
console.log("defaultValue", defaultValue);
console.log("parameterInput", parameterInput);
if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) {
let savedValue = operationSettings[operation][parameter.name]; let savedValue = operationSettings[operation][parameter.name];
@ -327,7 +413,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
parameterInput.value = savedValue; parameterInput.value = savedValue;
} }
} }
console.log("parameterInput2", parameterInput);
parameterDiv.appendChild(parameterInput); parameterDiv.appendChild(parameterInput);
pipelineSettingsContent.appendChild(parameterDiv); pipelineSettingsContent.appendChild(parameterDiv);
@ -340,50 +426,65 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
event.preventDefault(); event.preventDefault();
let settings = {}; let settings = {};
operationData.forEach(parameter => { operationData.forEach(parameter => {
let value = document.getElementById(parameter.name).value; if(parameter.name !== "fileInput"){
switch (parameter.schema.type) { let value = document.getElementById(parameter.name).value;
case 'number': switch (parameter.schema.type) {
case 'integer': case 'number':
settings[parameter.name] = Number(value); case 'integer':
break; settings[parameter.name] = Number(value);
case 'boolean': break;
settings[parameter.name] = document.getElementById(parameter.name).checked; case 'boolean':
break; settings[parameter.name] = document.getElementById(parameter.name).checked;
case 'array': break;
case 'object': case 'array':
try { case 'object':
settings[parameter.name] = JSON.parse(value); try {
} catch (err) { settings[parameter.name] = JSON.parse(value);
console.error(`Invalid JSON format for ${parameter.name}`); } catch (err) {
} console.error(`Invalid JSON format for ${parameter.name}`);
break; }
default: break;
settings[parameter.name] = value; default:
settings[parameter.name] = value;
}
} }
}); });
operationSettings[operation] = settings; operationSettings[operation] = settings;
console.log(settings); //pipelineSettingsModal.style.display = "none";
pipelineSettingsModal.style.display = "none";
}); });
pipelineSettingsContent.appendChild(saveButton); pipelineSettingsContent.appendChild(saveButton);
pipelineSettingsModal.style.display = "block"; //pipelineSettingsModal.style.display = "block";
pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { //pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() {
pipelineSettingsModal.style.display = "none"; // pipelineSettingsModal.style.display = "none";
} //}
window.onclick = function(event) { //window.onclick = function(event) {
if (event.target == pipelineSettingsModal) { // if (event.target == pipelineSettingsModal) {
pipelineSettingsModal.style.display = "none"; // pipelineSettingsModal.style.display = "none";
} // }
} //}
} }
hideOrShowPipelineHeader();
});
var saveBtn = document.getElementById('savePipelineBtn');
document.getElementById('savePipelineBtn').addEventListener('click', function() { // Remove any existing event listeners
saveBtn.removeEventListener('click', savePipeline);
// Add the event listener
saveBtn.addEventListener('click', savePipeline);
console.log("saveBtn", saveBtn)
function savePipeline() {
if (validatePipeline() === false) { if (validatePipeline() === false) {
return; return;
} }
var pipelineName = document.getElementById('pipelineName').value; var pipelineName = document.getElementById('pipelineName').value;
let pipelineList = document.getElementById('pipelineList').children; let pipelineList = document.getElementById('pipelineList').children;
let pipelineConfig = { let pipelineConfig = {
@ -393,31 +494,33 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
"outputDir": "{outputFolder}/{folderName}", "outputDir": "{outputFolder}/{folderName}",
"outputFileName": "{filename}-{pipelineName}-{date}-{time}" "outputFileName": "{filename}-{pipelineName}-{date}-{time}"
}, },
"outputDir": "httpWebRequest", "outputDir": "{outputFolder}",
"outputFileName": "{filename}" "outputFileName": "{filename}"
}; };
for (let i = 0; i < pipelineList.length; i++) { for (let i = 0; i < pipelineList.length; i++) {
let operationName = pipelineList[i].querySelector('.operationName').textContent; let operationName = pipelineList[i].querySelector('.operationName').textContent;
let parameters = operationSettings[operationName] || {}; let parameters = operationSettings[operationName] || {};
parameters['fileInput'] = 'automated';
pipelineConfig.pipeline.push({ pipelineConfig.pipeline.push({
"operation": operationName, "operation": operationName,
"parameters": parameters "parameters": parameters
}); });
} }
console.log("Downloading..");
let a = document.createElement('a'); let a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], {
type: 'application/json' type: 'application/json'
})); }));
a.download = 'pipelineConfig.json'; a.download = pipelineName + '.json';
a.style.display = 'none'; a.style.display = 'none';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
}); }
async function processPipelineConfig(configString) { async function processPipelineConfig(configString) {
let pipelineConfig = JSON.parse(configString); let pipelineConfig = JSON.parse(configString);
@ -483,6 +586,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
processPipelineConfig(event.target.result); processPipelineConfig(event.target.result);
}; };
reader.readAsText(e.target.files[0]); reader.readAsText(e.target.files[0]);
hideOrShowPipelineHeader();
}); });
document.getElementById('pipelineSelect').addEventListener('change', function(e) { document.getElementById('pipelineSelect').addEventListener('change', function(e) {
@ -491,4 +595,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function()
}); });
}); function hideOrShowPipelineHeader() {
var pipelineHeader = document.getElementById('pipelineHeader');
var pipelineList = document.getElementById('pipelineList');
if (pipelineList.children.length === 0) {
// Hide the pipeline header if there are no items in the pipeline list
pipelineHeader.style.display = 'none';
} else {
// Show the pipeline header if there are items in the pipeline list
pipelineHeader.style.display = 'block';
}
}

View File

@ -306,7 +306,7 @@
<div class="mb-3 mt-4"> <div class="mb-3 mt-4">
<a href="/logout"> <a href="logout">
<button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button> <button type="button" class="btn btn-danger" th:text="#{account.signOut}">Sign Out</button>
</a> </a>
<a th:if="${role == 'ROLE_ADMIN'}" href="addUsers" target="_blank"> <a th:if="${role == 'ROLE_ADMIN'}" href="addUsers" target="_blank">

View File

@ -62,6 +62,7 @@
<option value="ROLE_USER" th:text="#{adminUserSettings.user}">User</option> <option value="ROLE_USER" th:text="#{adminUserSettings.user}">User</option>
<option value="ROLE_LIMITED_API_USER" th:text="#{adminUserSettings.apiUser}">Limited API User</option> <option value="ROLE_LIMITED_API_USER" th:text="#{adminUserSettings.apiUser}">Limited API User</option>
<option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option> <option value="ROLE_WEB_ONLY_USER" th:text="#{adminUserSettings.webOnlyUser}">Web Only User</option>
<option value="ROLE_DEMO_USER" th:text="#{adminUserSettings.demoUser}">Demo User</option>
</select> </select>
</div> </div>
<div class="mb-3"> <div class="mb-3">

View File

@ -13,7 +13,7 @@
<div class="row justify-content-center"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<h2 th:text="#{PDFToCSV.header}"></h2> <h2 th:text="#{PDFToCSV.header}"></h2>
<form id="PDFToCSVForm" th:action="@{api/v1/convert/pdf-to-csv}" method="post" enctype="multipart/form-data"> <form id="PDFToCSVForm" th:action="@{api/v1/convert/pdf/csv}" method="post" enctype="multipart/form-data">
<input id="pageId" type="hidden" name="pageId" /> <input id="pageId" type="hidden" name="pageId" />
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div>
<button type="submit" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button> <button type="submit" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button>

View File

@ -32,12 +32,12 @@
<span class="icon-text" th:text="#{home.multiTool.title}"></span> <span class="icon-text" th:text="#{home.multiTool.title}"></span>
</a> </a>
</li> </li>
<!--<li class="nav-item"> <li th:if="${@enableAlphaFunctionality}" class="nav-item">
<a class="nav-link" href="#" th:href="@{pipeline}" th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}"> <a class="nav-link" href="#" th:href="@{pipeline}" th:classappend="${currentPage}=='pipeline' ? 'active' : ''" th:title="#{home.pipeline.desc}">
<img class="icon" src="images/pipeline.svg" alt="icon"> <img class="icon" src="images/pipeline.svg" alt="icon">
<span class="icon-text" th:text="#{home.pipeline.title}"></span> <span class="icon-text" th:text="#{home.pipeline.title}"></span>
</a> </a>
</li>--> </li>
<li class="nav-item nav-item-separator"></li> <li class="nav-item nav-item-separator"></li>
<li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''"> <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''">
@ -185,7 +185,7 @@
<!-- Search Bar --> <!-- Search Bar -->
<div class="collapse position-absolute" id="navbarSearch"> <div class="collapse position-absolute" id="navbarSearch">
<form class="d-flex p-2 bg-white border search-form" id="searchForm"> <form class="d-flex p-2 bg-white border search-form" id="searchForm">
<input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput"> <input class="form-control search-input" type="search" th:placeholder="#{home.searchBar}" aria-label="Search" id="navbarSearchInput">
</form> </form>
<!-- Search Results --> <!-- Search Results -->
<div id="searchResults" class="border p-2 bg-white search-results"></div> <div id="searchResults" class="border p-2 bg-white search-results"></div>
@ -293,7 +293,7 @@
</a> </a>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<a th:if="${@loginEnabled}" href="/logout"> <a th:if="${@loginEnabled}" href="logout">
<button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button> <button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button>
</a> </a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>

View File

@ -27,9 +27,10 @@
<input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}"> <input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}">
<div class="features-container "> <div class="features-container ">
<th:block th:if="${@enableAlphaFunctionality}">
<!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> --> <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div>
</th:block>
<div th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', svgPath='images/book-opened.svg')}"></div> <div th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', svgPath='images/book-opened.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> <div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div>
<div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div> <div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div>

View File

@ -35,7 +35,8 @@
<br> <br> <br> <br>
<div class="container"> <div class="container">
<div class="row justify-content-center"> <div class="row justify-content-center">
<h1>(Alpha) Pipeline Menu (Huge work in progress, very buggy!)</h1>
<div class="bordered-box"> <div class="bordered-box">
<div class="text-end text-top"> <div class="text-end text-top">
<button id="uploadPipelineBtn" class="btn btn-primary">Upload <button id="uploadPipelineBtn" class="btn btn-primary">Upload
@ -47,7 +48,6 @@
<div class="center-element"> <div class="center-element">
<div class="element-margin"> <div class="element-margin">
<select id="pipelineSelect" class="custom-select"> <select id="pipelineSelect" class="custom-select">
<option value="">Select a pipeline</option>
<th:block th:each="config : ${pipelineConfigsWithNames}"> <th:block th:each="config : ${pipelineConfigsWithNames}">
<option th:value="${config.json}" th:text="${config.name}"></option> <option th:value="${config.json}" th:text="${config.name}"></option>
</th:block> </th:block>
@ -62,16 +62,83 @@
</div> </div>
</div> </div>
<h3>Current Limitations</h3>
<ul>
<li>Cannot have more than one of the same operation</li>
<li>Cannot input additional files via UI</li>
<li>Does not work with multi-input functions yet (like merges)</li>
<li>All files and operations run in serial mode</li>
</ul>
<h3>How it Works Notes</h3>
<ul>
<li>Configure the pipeline config file and input files to run files against it</li>
<li>For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users</li>
</ul>
<h3>How to use pre-load configs in web UI</h3>
<ul>
<li>Download config files</li>
<li>For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users</li>
</ul>
<h3>Todo</h3>
<ul>
<li>fix initial config selected not loading</li>
<li>Fix operation adding requering settings to be openned and saved instead of saving defaults</li>
<li>multiInput support</li>
<li>Translation support</li>
<li>offline mode checks and testing</li>
<li>Improve operation config settings UI</li>
</ul>
<h2>User Guide for Local Directory Scanning and File Processing</h2>
<h3>Setting Up Watched Folders:</h3>
<p>Create a folder where you want your files to be monitored. This is your 'watched folder'.</p>
<p>The default directory for this is <code>./pipeline/watchedFolders/</code></p>
<p>Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.</p>
<h3>Configuring Processing with JSON Files:</h3>
<p>In each directory you want processed (e.g <code>./pipeline/watchedFolders/officePrinter</code>), include a JSON configuration file.</p>
<p>This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.</p>
<h3>Automatic Scanning and Processing:</h3>
<p>The system automatically checks the watched folder every minute for new directories and files to process.</p>
<p>When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.</p>
<h3>Processing Steps:</h3>
<p>Files in each directory are processed according to the instructions in the JSON file.</p>
<p>This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.</p>
<h3>Results and Output:</h3>
<p>After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location <code>./pipeline/finishedFolders/</code>.</p>
<p>Each processed file is named and organized according to the rules set in the JSON configuration.</p>
<h3>Completion and Cleanup:</h3>
<p>Once processing is complete, the original files in the watched folder's directory are removed.</p>
<p>You can find the processed files in the designated output location.</p>
<h3>Error Handling:</h3>
<p>If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.</p>
<h3>User Interaction:</h3>
<p>As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.</p>
<p>The system handles the rest, including scanning, processing, and outputting results.</p>
<!-- The Modal --> <!-- The Modal -->
<div class="modal" id="pipelineSettingsModal"> <div class="modal" id="pipelineSettingsModal">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content dark-card">
<!-- Modal Header --> <!-- Modal Header -->
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Pipeline Configuration</h2> <h2 class="modal-title">Pipeline Configuration</h2>
<button type="button" class="close" data-bs-dismiss="modal">&times;</button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<!-- Modal body --> <!-- Modal body -->
@ -90,7 +157,7 @@
<button id="addOperationBtn" class="btn btn-primary">Add <button id="addOperationBtn" class="btn btn-primary">Add
operation</button> operation</button>
</div> </div>
<h3>Pipeline:</h3> <h3 id="pipelineHeader" style="display: none;">Pipeline:</h3>
<ol id="pipelineList" class="list-group"> <ol id="pipelineList" class="list-group">
<!-- Pipeline operations will be dynamically populated here --> <!-- Pipeline operations will be dynamically populated here -->
</ol> </ol>