diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..ce4c63a7 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Formatting +5f771b785130154ed47952635b7acef371ffe0ec \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..74d1f8f5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,34 @@ +name: "Build repo" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + + - uses: gradle/gradle-build-action@v2.4.2 + with: + gradle-version: 7.6 + arguments: build --no-build-cache diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 55500892..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,55 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of - -name: "Build repo" - -on: - push: - branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] - schedule: - - cron: '15 12 * * 1' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - - # - name: Initialize CodeQL - # uses: github/codeql-action/init@v2 - # with: - # languages: java - - - uses: gradle/gradle-build-action@v2.4.2 - with: - gradle-version: 7.6 - arguments: assemble --no-build-cache - - #- name: Perform CodeQL analysis - # uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 67a9a4b5..b0c87c9e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,8 @@ local.properties .classpath .project version.properties -pipeline/ - +pipeline/watchedFolders/ +pipeline/finishedFolders/ #### Stirling-PDF Files ### customFiles/ configs/ diff --git a/Dockerfile b/Dockerfile index 6d830346..f0eb592c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,8 @@ ARG VERSION_TAG # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -18,13 +19,14 @@ ENV DOCKER_ENABLE_SECURITY=false \ ## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # 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 /usr/share/tesseract-ocr-original # Copy necessary files COPY ./scripts/* /scripts/ +COPY ./pipeline/ /pipeline/ 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 build/libs/*.jar app.jar @@ -42,4 +44,4 @@ EXPOSE 8080 # Set user and run command ##USER stirlingpdfuser ENTRYPOINT ["/scripts/init.sh"] -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/Dockerfile-lite b/Dockerfile-lite index 9d734942..054c7639 100644 --- a/Dockerfile-lite +++ b/Dockerfile-lite @@ -17,7 +17,8 @@ RUN apt-get update && \ # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -28,13 +29,14 @@ ENV DOCKER_ENABLE_SECURITY=false \ # mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME # 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 # Copy necessary files COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.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/*.otf /usr/share/fonts/opentype/noto/ COPY build/libs/*.jar app.jar @@ -60,4 +62,4 @@ ENV DOCKER_ENABLE_SECURITY=false # Run the application #USER stirlingpdfuser ENTRYPOINT ["/scripts/init-without-ocr.sh"] -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/Dockerfile-ultra-lite b/Dockerfile-ultra-lite index b49b9023..d77c1b94 100644 --- a/Dockerfile-ultra-lite +++ b/Dockerfile-ultra-lite @@ -6,7 +6,8 @@ ARG VERSION_TAG # Set Environment Variables ENV DOCKER_ENABLE_SECURITY=false \ HOME=/home/stirlingpdfuser \ - VERSION_TAG=$VERSION_TAG + VERSION_TAG=$VERSION_TAG \ + JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" # PUID=1000 \ # PGID=1000 \ # UMASK=022 \ @@ -18,12 +19,12 @@ ENV DOCKER_ENABLE_SECURITY=false \ # Set up necessary directories and permissions #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 COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh - +COPY ./pipeline/ /pipeline/ COPY build/libs/*.jar app.jar # Set font cache and permissions @@ -42,4 +43,4 @@ ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI ENTRYPOINT ["/scripts/init-without-ocr.sh"] # Run the application -CMD ["java", "-jar", "/app.jar"] +CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] diff --git a/README.md b/README.md index 5a37bb30..d9515ca6 100644 --- a/README.md +++ b/README.md @@ -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 ## Want to add your own language? -Stirling PDF currently supports 20! +Stirling PDF currently supports 21! - English (English) (en_GB) - English (US) (en_US) - Arabic (العربية) (ar_AR) @@ -169,6 +169,7 @@ Stirling PDF currently supports 20! - Greek (el_GR) - Turkish (Türkçe) (tr_TR) - Indonesia (Bahasa Indonesia) (id_ID) +- Hindi (हिंदी) (hi_IN) If you want to add your own language to Stirling-PDF please refer https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md diff --git a/build.gradle b/build.gradle index 66de762e..3bf48dcb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,19 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.1.2' - id 'io.spring.dependency-management' version '1.1.3' - id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' - id "io.swagger.swaggerhub" version "1.3.2" - id 'edu.sc.seis.launch4j' version '3.0.5' + id 'java' + id 'org.springframework.boot' version '3.1.2' + id 'io.spring.dependency-management' version '1.1.3' + id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' + id "io.swagger.swaggerhub" version "1.3.2" + id 'edu.sc.seis.launch4j' version '3.0.5' + id 'com.diffplug.spotless' version '6.23.3' } group = 'stirling.software' -version = '0.17.2' +version = '0.18.1' sourceCompatibility = '17' repositories { - mavenCentral() + mavenCentral() } sourceSets { @@ -48,7 +49,7 @@ launch4j { errTitle="Encountered error, Do you have Java 17?" 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" mutexName="Stirling-PDF" @@ -61,24 +62,38 @@ launch4j { messagesInstanceAlreadyExists="Stirling-PDF is already running." } +spotless { + java { + target project.fileTree('src/main/java') + + googleJavaFormat('1.19.1').aosp().reorderImports(false) + + importOrder('java', 'javax', 'org', 'com', 'net', 'io') + toggleOffOn() + trimTrailingWhitespace() + indentWithSpaces() + endWithNewline() + } +} + dependencies { - //security updates - implementation 'ch.qos.logback:logback-classic:1.4.14' - implementation 'ch.qos.logback:logback-core:1.4.14' - implementation 'org.springframework:spring-webmvc:6.0.15' - + //security updates + implementation 'ch.qos.logback:logback-classic:1.4.14' + implementation 'ch.qos.logback:logback-core:1.4.14' + implementation 'org.springframework:spring-webmvc:6.0.15' + implementation 'org.yaml:snakeyaml:2.1' - implementation 'org.springframework.boot:spring-boot-starter-web:3.1.6' - implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.6' + implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1' 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.springframework.boot:spring-boot-starter-data-jpa" 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 implementation 'org.apache.xmlgraphics:batik-all:1.17' @@ -134,6 +149,9 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.28' } +tasks.withType(JavaCompile) { + dependsOn 'spotlessApply' +} task writeVersion { def propsFile = file('src/main/resources/version.properties') @@ -164,7 +182,7 @@ jar { } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } task printVersion { diff --git a/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json b/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json new file mode 100644 index 00000000..ba3228dc --- /dev/null +++ b/pipeline/defaultWebUIConfigs/Prepare-pdfs-for-email.json @@ -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}" +} \ No newline at end of file diff --git a/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json b/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json new file mode 100644 index 00000000..3a989296 --- /dev/null +++ b/pipeline/defaultWebUIConfigs/split-rotate-auto-rename.json @@ -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}" +} \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java index 7f4fc160..6d32adc3 100644 --- a/src/main/java/stirling/software/SPDF/LibreOfficeListener.java +++ b/src/main/java/stirling/software/SPDF/LibreOfficeListener.java @@ -22,14 +22,14 @@ public class LibreOfficeListener { private Process process; - private LibreOfficeListener() { - } + private LibreOfficeListener() {} private boolean isListenerRunning() { try { System.out.println("waiting for listener to start"); Socket socket = new Socket(); - socket.connect(new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second + socket.connect( + new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second socket.close(); return true; } catch (IOException e) { @@ -49,21 +49,22 @@ public class LibreOfficeListener { // Start a background thread to monitor the activity timeout executorService = Executors.newSingleThreadExecutor(); - executorService.submit(() -> { - while (true) { - long idleTime = System.currentTimeMillis() - lastActivityTime; - if (idleTime >= ACTIVITY_TIMEOUT) { - // If there has been no activity for too long, tear down the listener - process.destroy(); - break; - } - try { - Thread.sleep(5000); // Check for inactivity every 5 seconds - } catch (InterruptedException e) { - break; - } - } - }); + executorService.submit( + () -> { + while (true) { + long idleTime = System.currentTimeMillis() - lastActivityTime; + if (idleTime >= ACTIVITY_TIMEOUT) { + // If there has been no activity for too long, tear down the listener + process.destroy(); + break; + } + try { + Thread.sleep(5000); // Check for inactivity every 5 seconds + } catch (InterruptedException e) { + break; + } + } + }); // Wait for the listener to start up long startTime = System.currentTimeMillis(); @@ -92,5 +93,4 @@ public class LibreOfficeListener { process.destroy(); } } - } diff --git a/src/main/java/stirling/software/SPDF/SPdfApplication.java b/src/main/java/stirling/software/SPDF/SPdfApplication.java index 5dd7fed3..ef3733c6 100644 --- a/src/main/java/stirling/software/SPDF/SPdfApplication.java +++ b/src/main/java/stirling/software/SPDF/SPdfApplication.java @@ -8,17 +8,17 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.env.Environment; +import org.springframework.scheduling.annotation.EnableScheduling; import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.ConfigInitializer; import stirling.software.SPDF.utils.GeneralUtils; -@SpringBootApplication -//@EnableScheduling +@SpringBootApplication +@EnableScheduling public class SPdfApplication { - - @Autowired - private Environment env; + + @Autowired private Environment env; @PostConstruct public void init() { @@ -28,11 +28,7 @@ public class SPdfApplication { if (browserOpen) { try { - String port = env.getProperty("local.server.port"); - if(port == null || port.length() == 0) { - port="8080"; - } - String url = "http://localhost:" + port; + String url = "http://localhost:" + getPort(); String os = System.getProperty("os.name").toLowerCase(); Runtime rt = Runtime.getRuntime(); @@ -45,38 +41,41 @@ public class SPdfApplication { } } } - + public static void main(String[] args) { - SpringApplication app = new SpringApplication(SPdfApplication.class); - app.addInitializers(new ConfigInitializer()); - if (Files.exists(Paths.get("configs/settings.yml"))) { - app.setDefaultProperties(Collections.singletonMap("spring.config.additional-location", "file:configs/settings.yml")); + SpringApplication app = new SpringApplication(SPdfApplication.class); + app.addInitializers(new ConfigInitializer()); + if (Files.exists(Paths.get("configs/settings.yml"))) { + app.setDefaultProperties( + Collections.singletonMap( + "spring.config.additional-location", "file:configs/settings.yml")); } else { - 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); - + try { - Thread.sleep(1000); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - + Thread.sleep(1000); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + GeneralUtils.createDir("customFiles/static/"); GeneralUtils.createDir("customFiles/templates/"); - - - + System.out.println("Stirling-PDF Started."); - - String port = System.getProperty("local.server.port"); - if(port == null || port.length() == 0) { - port="8080"; - } - String url = "http://localhost:" + port; + + String url = "http://localhost:" + getPort(); System.out.println("Navigate to " + url); } - - -} \ No newline at end of file + + public static String getPort() { + String port = System.getProperty("local.server.port"); + if (port == null || port.isEmpty()) { + port = "8080"; + } + return port; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 4d02f974..273de957 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -5,13 +5,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import stirling.software.SPDF.model.ApplicationProperties; + @Configuration public class AppConfig { - - @Autowired - ApplicationProperties applicationProperties; - + @Autowired ApplicationProperties applicationProperties; + @Bean(name = "loginEnabled") public boolean loginEnabled() { return applicationProperties.getSecurity().getEnableLogin(); @@ -19,7 +18,7 @@ public class AppConfig { @Bean(name = "appName") public String appName() { - String homeTitle = applicationProperties.getUi().getAppName(); + String homeTitle = applicationProperties.getUi().getAppName(); return (homeTitle != null) ? homeTitle : "Stirling PDF"; } @@ -31,24 +30,31 @@ public class AppConfig { @Bean(name = "homeText") public String homeText() { - return (applicationProperties.getUi().getHomeDescription() != null) ? applicationProperties.getUi().getHomeDescription() : "null"; + return (applicationProperties.getUi().getHomeDescription() != null) + ? applicationProperties.getUi().getHomeDescription() + : "null"; } - @Bean(name = "navBarText") public String navBarText() { - String defaultNavBar = applicationProperties.getUi().getAppNameNavbar() != null ? applicationProperties.getUi().getAppNameNavbar() : applicationProperties.getUi().getAppName(); + String defaultNavBar = + applicationProperties.getUi().getAppNameNavbar() != null + ? applicationProperties.getUi().getAppNameNavbar() + : applicationProperties.getUi().getAppName(); return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF"; } - - @Bean(name = "rateLimit") + + @Bean(name = "enableAlphaFunctionality") + public boolean enableAlphaFunctionality() { + return applicationProperties.getSystem().getEnableAlphaFunctionality() != null + ? applicationProperties.getSystem().getEnableAlphaFunctionality() + : false; + } + + @Bean(name = "rateLimit") public boolean rateLimit() { String appName = System.getProperty("rateLimit"); - if (appName == null) - appName = System.getenv("rateLimit"); - System.out.println("rateLimit=" + appName); + if (appName == null) appName = System.getenv("rateLimit"); return (appName != null) ? Boolean.valueOf(appName) : false; } - - -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/Beans.java b/src/main/java/stirling/software/SPDF/config/Beans.java index d1c6a03b..9230a0a0 100644 --- a/src/main/java/stirling/software/SPDF/config/Beans.java +++ b/src/main/java/stirling/software/SPDF/config/Beans.java @@ -15,10 +15,9 @@ import stirling.software.SPDF.model.ApplicationProperties; @Configuration public class Beans implements WebMvcConfigurer { - - @Autowired - ApplicationProperties applicationProperties; - + + @Autowired ApplicationProperties applicationProperties; + @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(localeChangeInterceptor()); @@ -35,25 +34,26 @@ public class Beans implements WebMvcConfigurer { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver slr = new SessionLocaleResolver(); - - + String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); - Locale defaultLocale = Locale.UK; // Fallback to UK locale if environment variable is not set + Locale defaultLocale = + Locale.UK; // Fallback to UK locale if environment variable is not set if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); String tempLanguageTag = tempLocale.toLanguageTag(); - if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { + if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale; } else { - tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_","-")); + tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-")); tempLanguageTag = tempLocale.toLanguageTag(); if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { defaultLocale = tempLocale; } else { - System.err.println("Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); + System.err.println( + "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); } } } @@ -61,5 +61,4 @@ public class Beans implements WebMvcConfigurer { slr.setDefaultLocale(defaultLocale); return slr; } - } diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index 894d50d8..472fb951 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -13,56 +13,62 @@ import jakarta.servlet.http.HttpServletResponse; public class CleanUrlInterceptor implements HandlerInterceptor { - private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); + private static final List ALLOWED_PARAMS = + Arrays.asList( + "lang", "endpoint", "endpoints", "logout", "error", "file", "messageType"); - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - Map parameters = new HashMap<>(); + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String queryString = request.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + String requestURI = request.getRequestURI(); + Map parameters = new HashMap<>(); - // Keep only the allowed parameters - String[] queryParameters = queryString.split("&"); - for (String param : queryParameters) { - String[] keyValue = param.split("="); - if (keyValue.length != 2) { - continue; - } - if (ALLOWED_PARAMS.contains(keyValue[0])) { - parameters.put(keyValue[0], keyValue[1]); - } - } + // Keep only the allowed parameters + String[] queryParameters = queryString.split("&"); + for (String param : queryParameters) { + String[] keyValue = param.split("="); + if (keyValue.length != 2) { + continue; + } + if (ALLOWED_PARAMS.contains(keyValue[0])) { + parameters.put(keyValue[0], keyValue[1]); + } + } - // If there are any parameters that are not allowed - if (parameters.size() != queryParameters.length) { - // Construct new query string - StringBuilder newQueryString = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { - if (newQueryString.length() > 0) { - newQueryString.append("&"); - } - newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); - } + // If there are any parameters that are not allowed + if (parameters.size() != queryParameters.length) { + // Construct new query string + StringBuilder newQueryString = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (newQueryString.length() > 0) { + newQueryString.append("&"); + } + newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); + } - // Redirect to the URL with only allowed query parameters - String redirectUrl = requestURI + "?" + newQueryString; - response.sendRedirect(redirectUrl); - return false; - } - } - return true; - } + // Redirect to the URL with only allowed query parameters + String redirectUrl = requestURI + "?" + newQueryString; + response.sendRedirect(redirectUrl); + return false; + } + } + return true; + } - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) { - } + @Override + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView) {} - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { - } + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception ex) {} } diff --git a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java index 862f5718..6435c955 100644 --- a/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java +++ b/src/main/java/stirling/software/SPDF/config/ConfigInitializer.java @@ -19,111 +19,125 @@ import java.util.stream.Collectors; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -public class ConfigInitializer implements ApplicationContextInitializer { +public class ConfigInitializer + implements ApplicationContextInitializer { - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - try { - ensureConfigExists(); - } catch (IOException e) { - throw new RuntimeException("Failed to initialize application configuration", e); - } - } + @Override + public void initialize(ConfigurableApplicationContext applicationContext) { + try { + ensureConfigExists(); + } catch (IOException e) { + throw new RuntimeException("Failed to initialize application configuration", e); + } + } - public void ensureConfigExists() throws IOException { - // Define the path to the external config directory - Path destPath = Paths.get("configs", "settings.yml"); + public void ensureConfigExists() throws IOException { + // Define the path to the external config directory + Path destPath = Paths.get("configs", "settings.yml"); - // Check if the file already exists - if (Files.notExists(destPath)) { - // Ensure the destination directory exists - Files.createDirectories(destPath.getParent()); + // Check if the file already exists + if (Files.notExists(destPath)) { + // Ensure the destination directory exists + Files.createDirectories(destPath.getParent()); - // Copy the resource from classpath to the external directory - try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { - if (in != null) { - Files.copy(in, destPath); - } else { - throw new FileNotFoundException("Resource file not found: settings.yml.template"); - } - } - } else { - // If user file exists, we need to merge it with the template from the classpath - List templateLines; - try (InputStream in = getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { - templateLines = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)).lines() - .collect(Collectors.toList()); - } + // Copy the resource from classpath to the external directory + try (InputStream in = + getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { + if (in != null) { + Files.copy(in, destPath); + } else { + throw new FileNotFoundException( + "Resource file not found: settings.yml.template"); + } + } + } else { + // If user file exists, we need to merge it with the template from the classpath + List templateLines; + try (InputStream in = + getClass().getClassLoader().getResourceAsStream("settings.yml.template")) { + templateLines = + new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8)) + .lines() + .collect(Collectors.toList()); + } - mergeYamlFiles(templateLines, destPath, destPath); - } - } + mergeYamlFiles(templateLines, destPath, destPath); + } + } - public void mergeYamlFiles(List templateLines, Path userFilePath, Path outputPath) throws IOException { - List userLines = Files.readAllLines(userFilePath); - List mergedLines = new ArrayList<>(); - boolean insideAutoGenerated = false; - boolean beforeFirstKey = true; + public void mergeYamlFiles(List templateLines, Path userFilePath, Path outputPath) + throws IOException { + List userLines = Files.readAllLines(userFilePath); + List mergedLines = new ArrayList<>(); + boolean insideAutoGenerated = false; + boolean beforeFirstKey = true; - Function isCommented = line -> line.trim().startsWith("#"); - Function extractKey = line -> { - String[] parts = line.split(":"); - return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; - }; + Function isCommented = line -> line.trim().startsWith("#"); + Function extractKey = + line -> { + String[] parts = line.split(":"); + return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : ""; + }; - Set userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); + Set userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet()); - for (String line : templateLines) { - String key = extractKey.apply(line); + for (String line : templateLines) { + String key = extractKey.apply(line); - if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { - insideAutoGenerated = true; - mergedLines.add(line); - continue; - } else if (insideAutoGenerated && line.trim().isEmpty()) { - insideAutoGenerated = false; - mergedLines.add(line); - continue; - } + if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) { + insideAutoGenerated = true; + mergedLines.add(line); + continue; + } else if (insideAutoGenerated && line.trim().isEmpty()) { + insideAutoGenerated = false; + mergedLines.add(line); + continue; + } - if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { - // Handle top comments and empty lines before the first key. - mergedLines.add(line); - continue; - } + if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) { + // Handle top comments and empty lines before the first key. + mergedLines.add(line); + continue; + } - if (!key.isEmpty()) - beforeFirstKey = false; + if (!key.isEmpty()) beforeFirstKey = false; - if (userKeys.contains(key)) { - // If user has any version (commented or uncommented) of this key, skip the - // template line - Optional userValue = userLines.stream() - .filter(l -> extractKey.apply(l).equalsIgnoreCase(key) && !isCommented.apply(l)).findFirst(); - if (userValue.isPresent()) - mergedLines.add(userValue.get()); - continue; - } + if (userKeys.contains(key)) { + // If user has any version (commented or uncommented) of this key, skip the + // template line + Optional userValue = + userLines.stream() + .filter( + l -> + extractKey.apply(l).equalsIgnoreCase(key) + && !isCommented.apply(l)) + .findFirst(); + if (userValue.isPresent()) mergedLines.add(userValue.get()); + continue; + } - if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { - mergedLines.add(line); // If line is commented, empty or key not present in user's file, retain the - // template line - continue; - } - } + if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) { + mergedLines.add( + line); // If line is commented, empty or key not present in user's file, + // retain the + // template line + continue; + } + } - // Add any additional uncommented user lines that are not present in the - // template - for (String userLine : userLines) { - String userKey = extractKey.apply(userLine); - boolean isPresentInTemplate = templateLines.stream().map(extractKey) - .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); - if (!isPresentInTemplate && !isCommented.apply(userLine)) { - mergedLines.add(userLine); - } - } + // Add any additional uncommented user lines that are not present in the + // template + for (String userLine : userLines) { + String userKey = extractKey.apply(userLine); + boolean isPresentInTemplate = + templateLines.stream() + .map(extractKey) + .anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey)); + if (!isPresentInTemplate && !isCommented.apply(userLine)) { + mergedLines.add(userLine); + } + } - Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); - } - -} \ No newline at end of file + Files.write(outputPath, mergedLines, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index ebba9815..ddba4623 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import stirling.software.SPDF.model.ApplicationProperties; + @Service public class EndpointConfiguration { private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class); @@ -26,16 +27,16 @@ public class EndpointConfiguration { init(); processEnvironmentConfigs(); } - + public void enableEndpoint(String endpoint) { - endpointStatuses.put(endpoint, true); + endpointStatuses.put(endpoint, true); } public void disableEndpoint(String endpoint) { - if(!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { - logger.info("Disabling {}", endpoint); - endpointStatuses.put(endpoint, false); - } + if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { + logger.info("Disabling {}", endpoint); + endpointStatuses.put(endpoint, false); + } } public boolean isEndpointEnabled(String endpoint) { @@ -66,7 +67,7 @@ public class EndpointConfiguration { } } } - + public void init() { // Adding endpoints to "PageOps" group addEndpointToGroup("PageOps", "remove-pages"); @@ -84,8 +85,7 @@ public class EndpointConfiguration { addEndpointToGroup("PageOps", "split-by-size-or-count"); addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("PageOps", "split-pdf-by-sections"); - - + // Adding endpoints to "Convert" group addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "img-to-pdf"); @@ -101,8 +101,7 @@ public class EndpointConfiguration { addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Convert", "pdf-to-csv"); - - + // Adding endpoints to "Security" group addEndpointToGroup("Security", "add-password"); addEndpointToGroup("Security", "remove-password"); @@ -111,8 +110,7 @@ public class EndpointConfiguration { addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Security", "auto-redact"); - - + // Adding endpoints to "Other" group addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "add-image"); @@ -130,10 +128,8 @@ public class EndpointConfiguration { addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("Other", "show-javascript"); - - - - //CLI + + // CLI addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("CLI", "remove-blanks"); @@ -149,19 +145,18 @@ public class EndpointConfiguration { addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "url-to-pdf"); - - - //python + + // python addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("Python", "remove-blanks"); addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "url-to-pdf"); - - //openCV + + // openCV addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "remove-blanks"); - //LibreOffice + // LibreOffice addEndpointToGroup("LibreOffice", "repair"); addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); @@ -170,14 +165,13 @@ public class EndpointConfiguration { addEndpointToGroup("LibreOffice", "pdf-to-text"); addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-xml"); - - - //OCRmyPDF + + // OCRmyPDF addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("OCRmyPDF", "ocr-pdf"); - - //Java + + // Java addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "remove-pages"); addEndpointToGroup("Java", "split-pdfs"); @@ -210,16 +204,14 @@ public class EndpointConfiguration { addEndpointToGroup("Java", "split-by-size-or-count"); addEndpointToGroup("Java", "overlay-pdf"); addEndpointToGroup("Java", "split-pdf-by-sections"); - - //Javascript + + // Javascript addEndpointToGroup("Javascript", "pdf-organizer"); addEndpointToGroup("Javascript", "sign"); addEndpointToGroup("Javascript", "compare"); addEndpointToGroup("Javascript", "adjust-contrast"); - - } - + private void processEnvironmentConfigs() { List endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); List groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove(); @@ -236,6 +228,4 @@ public class EndpointConfiguration { } } } - } - diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 77191a41..d408b9ea 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -10,11 +10,11 @@ import jakarta.servlet.http.HttpServletResponse; @Component public class EndpointInterceptor implements HandlerInterceptor { - @Autowired - private EndpointConfiguration endpointConfiguration; + @Autowired private EndpointConfiguration endpointConfiguration; @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); if (!endpointConfiguration.isEndpointEnabled(requestURI)) { @@ -23,4 +23,4 @@ public class EndpointInterceptor implements HandlerInterceptor { } return true; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java index 1cdc99e3..3877c566 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -21,4 +22,4 @@ public class MetricsConfig { } }; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 859c9c50..9c17f3af 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -8,6 +8,7 @@ import org.springframework.web.filter.OncePerRequestFilter; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -24,22 +25,39 @@ public class MetricsFilter extends OncePerRequestFilter { } @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String uri = request.getRequestURI(); + + // System.out.println("uri="+uri + ", method=" + request.getMethod() ); // Ignore static resources - if (!(uri.startsWith("/api/v1/info") || 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"))) { - Counter counter = Counter.builder("http.requests") - .tag("uri", uri) - .tag("method", request.getMethod()) - .register(meterRegistry); + 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); counter.increment(); } filterChain.doFilter(request, response); } - - - } diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 1316792c..a852bb1a 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -9,34 +9,45 @@ import io.swagger.v3.oas.models.OpenAPI; 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 public class OpenApiConfig { - @Autowired - ApplicationProperties applicationProperties; + @Autowired ApplicationProperties applicationProperties; - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - 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")); - } - - } + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + 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")); + } + } } diff --git a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java index 77b69c88..07644b30 100644 --- a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java +++ b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.config; - import java.time.LocalDateTime; import org.springframework.context.ApplicationListener; @@ -17,4 +16,3 @@ public class StartupApplicationListener implements ApplicationListener createPropertySource(String name, EncodedResource encodedResource) - throws IOException { + public PropertySource createPropertySource(String name, EncodedResource encodedResource) + throws IOException { YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean(); factory.setResources(encodedResource.getResource()); Properties properties = factory.getObject(); - return new PropertiesPropertySource(encodedResource.getResource().getFilename(), properties); + return new PropertiesPropertySource( + encodedResource.getResource().getFilename(), properties); } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java index f286f149..6c2a05d3 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationFailureHandler.java @@ -2,27 +2,48 @@ package stirling.software.SPDF.config.security; import java.io.IOException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; +import org.springframework.stereotype.Component; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +@Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { + @Autowired private final LoginAttemptService loginAttemptService; + + @Autowired + public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) { + this.loginAttemptService = loginAttemptService; + } + @Override - public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) - throws IOException, ServletException { - String ip = request.getRemoteAddr(); + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception) + throws IOException, ServletException { + String ip = request.getRemoteAddr(); logger.error("Failed login attempt from IP: " + ip); - if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) { - setDefaultFailureUrl("/login?error=badcredentials"); - } else if (exception.getClass().isAssignableFrom(LockedException.class)) { - setDefaultFailureUrl("/login?error=locked"); + + String username = request.getParameter("username"); + if (loginAttemptService.loginAttemptCheck(username)) { + 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); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java new file mode 100644 index 00000000..d14466ea --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/CustomAuthenticationSuccessHandler.java @@ -0,0 +1,47 @@ +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); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java index 7ae1680b..021cdc31 100644 --- a/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java +++ b/src/main/java/stirling/software/SPDF/config/security/CustomUserDetailsService.java @@ -5,6 +5,7 @@ import java.util.Set; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.LockedException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; @@ -19,27 +20,38 @@ import stirling.software.SPDF.repository.UserRepository; @Service public class CustomUserDetailsService implements UserDetailsService { - @Autowired - private UserRepository userRepository; + @Autowired private UserRepository userRepository; + + @Autowired private LoginAttemptService loginAttemptService; - @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("No user found with username: " + username)); + User user = + userRepository + .findByUsername(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( - user.getUsername(), - user.getPassword(), - user.isEnabled(), - true, true, true, - getAuthorities(user.getAuthorities()) - ); + user.getUsername(), + user.getPassword(), + user.isEnabled(), + true, + true, + true, + getAuthorities(user.getAuthorities())); } private Collection getAuthorities(Set authorities) { return authorities.stream() - .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); + .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); } } diff --git a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java index 8e612464..b272327a 100644 --- a/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/FirstLoginFilter.java @@ -15,35 +15,35 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.User; +import stirling.software.SPDF.utils.RequestUriUtils; @Component public class FirstLoginFilter extends OncePerRequestFilter { - - @Autowired - @Lazy - private UserService userService; - + + @Autowired @Lazy private UserService userService; + @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String method = request.getMethod(); - String requestURI = request.getRequestURI(); - // Check if the request is for static resources - boolean isStaticResource = requestURI.startsWith("/css/") - || requestURI.startsWith("/js/") - || requestURI.startsWith("/images/") - || requestURI.startsWith("/public/") - || requestURI.endsWith(".svg"); + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String method = request.getMethod(); + String requestURI = request.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) { filterChain.doFilter(request, response); return; } - + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { Optional user = userService.findByUsername(authentication.getName()); - if ("GET".equalsIgnoreCase(method) && user.isPresent() && user.get().isFirstLogin() && !"/change-creds".equals(requestURI)) { + if ("GET".equalsIgnoreCase(method) + && user.isPresent() + && user.get().isFirstLogin() + && !"/change-creds".equals(requestURI)) { response.sendRedirect("/change-creds"); return; } diff --git a/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java new file mode 100644 index 00000000..b79cd586 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/IPRateLimitingFilter.java @@ -0,0 +1,68 @@ +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 requestCounts = + new ConcurrentHashMap<>(); + private final ConcurrentHashMap 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(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 842375d8..3b396b15 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -13,73 +13,76 @@ import org.springframework.stereotype.Component; import jakarta.annotation.PostConstruct; import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.Role; + @Component public class InitialSecuritySetup { - @Autowired - private UserService userService; + @Autowired private UserService userService; + @Autowired ApplicationProperties applicationProperties; - @Autowired - ApplicationProperties applicationProperties; - - @PostConstruct - public void init() { - if (!userService.hasUsers()) { - - - String initialUsername = applicationProperties.getSecurity().getInitialLogin().getUsername(); - String initialPassword = applicationProperties.getSecurity().getInitialLogin().getPassword(); - if (initialUsername != null && initialPassword != null) { - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); - } else { - initialUsername = "admin"; - initialPassword = "stirling"; - userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId(), true); - } - - - } - } + @PostConstruct + public void init() { + if (!userService.hasUsers()) { + String initialUsername = + applicationProperties.getSecurity().getInitialLogin().getUsername(); + String initialPassword = + applicationProperties.getSecurity().getInitialLogin().getPassword(); + if (initialUsername != null && initialPassword != null) { + userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); + } else { + initialUsername = "admin"; + initialPassword = "stirling"; + 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()); + } + } + @PostConstruct + public void initSecretKey() throws IOException { + String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); + if (secretKey == null || secretKey.isEmpty()) { + secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key + saveKeyToConfig(secretKey); + } + } - @PostConstruct - public void initSecretKey() throws IOException { - String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); - if (secretKey == null || secretKey.isEmpty()) { - secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key - saveKeyToConfig(secretKey); - } - } + private void saveKeyToConfig(String key) throws IOException { + Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml + List lines = Files.readAllLines(path); + boolean keyFound = false; - private void saveKeyToConfig(String key) throws IOException { - Path path = Paths.get("configs", "settings.yml"); // Target the configs/settings.yml - List lines = Files.readAllLines(path); - boolean keyFound = false; + // Search for the existing key to replace it or place to add it + for (int i = 0; i < lines.size(); i++) { + if (lines.get(i).startsWith("AutomaticallyGenerated:")) { + keyFound = true; + if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { + lines.set(i + 1, " key: " + key); + break; + } else { + lines.add(i + 1, " key: " + key); + break; + } + } + } - // Search for the existing key to replace it or place to add it - for (int i = 0; i < lines.size(); i++) { - if (lines.get(i).startsWith("AutomaticallyGenerated:")) { - keyFound = true; - if (i + 1 < lines.size() && lines.get(i + 1).trim().startsWith("key:")) { - lines.set(i + 1, " key: " + key); - break; - } else { - lines.add(i + 1, " key: " + key); - break; - } - } - } + // If the section doesn't exist, append it + if (!keyFound) { + lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); + lines.add("AutomaticallyGenerated:"); + lines.add(" key: " + key); + } - // If the section doesn't exist, append it - if (!keyFound) { - lines.add("# Automatically Generated Settings (Do Not Edit Directly)"); - lines.add("AutomaticallyGenerated:"); - lines.add(" key: " + key); - } - - // Write back to the file - Files.write(path, lines); - } -} \ No newline at end of file + // Write back to the file + Files.write(path, lines); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java new file mode 100644 index 00000000..40a54ecc --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/LoginAttemptService.java @@ -0,0 +1,58 @@ +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 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; + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java new file mode 100644 index 00000000..a3641a70 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/security/RateLimitResetScheduler.java @@ -0,0 +1,19 @@ +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(); + } +} diff --git a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java index dfe782ac..2b7ed6d1 100644 --- a/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/security/SecurityConfiguration.java @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; 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.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; @@ -15,78 +15,113 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import stirling.software.SPDF.repository.JPATokenRepositoryImpl; + @Configuration @EnableWebSecurity() -@EnableGlobalMethodSecurity(prePostEnabled = true) +@EnableMethodSecurity public class SecurityConfiguration { - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Autowired - @Lazy - private UserService userService; - + + @Autowired @Lazy private UserService userService; + @Autowired @Qualifier("loginEnabled") public boolean loginEnabledValue; - - @Autowired - private UserAuthenticationFilter userAuthenticationFilter; - - @Autowired - private FirstLoginFilter firstLoginFilter; - + + @Autowired private UserAuthenticationFilter userAuthenticationFilter; + + @Autowired private LoginAttemptService loginAttemptService; + + @Autowired private FirstLoginFilter firstLoginFilter; + @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); - - if(loginEnabledValue) { - - http.csrf(csrf -> csrf.disable()); - http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); - http - .formLogin(formLogin -> formLogin - .loginPage("/login") - .defaultSuccessUrl("/") - .failureHandler(new CustomAuthenticationFailureHandler()) - .permitAll() - ) - .logout(logout -> logout - .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) - .logoutSuccessUrl("/login?logout=true") - .invalidateHttpSession(true) // Invalidate session - .deleteCookies("JSESSIONID", "remember-me") - ).rememberMe(rememberMeConfigurer -> rememberMeConfigurer // Use the configurator directly - .key("uniqueAndSecret") - .tokenRepository(persistentTokenRepository()) - .tokenValiditySeconds(1209600) // 2 weeks - ) - .authorizeHttpRequests(authz -> authz - .requestMatchers(req -> 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/")) - .permitAll() - .anyRequest().authenticated() - ) - .userDetailsService(userDetailsService) - .authenticationProvider(authenticationProvider()); - } else { - http.csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authz -> authz - .anyRequest().permitAll() - ); - } + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + if (loginEnabledValue) { + + http.csrf(csrf -> csrf.disable()); + http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); + http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); + http.formLogin( + formLogin -> + formLogin + .loginPage("/login") + .successHandler( + new CustomAuthenticationSuccessHandler()) + .defaultSuccessUrl("/") + .failureHandler( + new CustomAuthenticationFailureHandler( + loginAttemptService)) + .permitAll()) + .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())) + .logout( + logout -> + logout.logoutRequestMatcher( + new AntPathRequestMatcher("/logout")) + .logoutSuccessUrl("/login?logout=true") + .invalidateHttpSession(true) // Invalidate session + .deleteCookies("JSESSIONID", "remember-me")) + .rememberMe( + rememberMeConfigurer -> + rememberMeConfigurer // Use the configurator directly + .key("uniqueAndSecret") + .tokenRepository(persistentTokenRepository()) + .tokenValiditySeconds(1209600) // 2 weeks + ) + .authorizeHttpRequests( + authz -> + authz.requestMatchers( + req -> { + String uri = req.getRequestURI(); + String contextPath = req.getContextPath(); + + // Remove the context path from the URI + String trimmedUri = + uri.startsWith(contextPath) + ? uri.substring( + contextPath + .length()) + : uri; + + 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()) + .userDetailsService(userDetailsService) + .authenticationProvider(authenticationProvider()); + } else { + http.csrf(csrf -> csrf.disable()) + .authorizeHttpRequests(authz -> authz.anyRequest().permitAll()); + } return http.build(); } + @Bean + public IPRateLimitingFilter rateLimitingFilter() { + int maxRequestsPerIp = 1000000; // Example limit TODO add config level + return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp); + } - @Bean public DaoAuthenticationProvider authenticationProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); @@ -94,13 +129,9 @@ public class SecurityConfiguration { authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; } - + @Bean public PersistentTokenRepository persistentTokenRepository() { return new JPATokenRepositoryImpl(); } - - - } - diff --git a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java index eca7f70e..b4375568 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserAuthenticationFilter.java @@ -19,32 +19,29 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.ApiKeyAuthenticationToken; + @Component public class UserAuthenticationFilter extends OncePerRequestFilter { - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; + + @Autowired @Lazy private UserService userService; - @Autowired - @Lazy - private UserService userService; - - @Autowired @Qualifier("loginEnabled") public boolean loginEnabledValue; @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { if (!loginEnabledValue) { // If login is not enabled, just pass all requests without authentication filterChain.doFilter(request, response); return; } - String requestURI = request.getRequestURI(); + String requestURI = request.getRequestURI(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // Check for API key in the request headers if no authentication exists @@ -52,15 +49,17 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.trim().isEmpty()) { try { - // Use API key to authenticate. This requires you to have an authentication provider for API keys. - UserDetails userDetails = userService.loadUserByApiKey(apiKey); - if(userDetails == null) - { - response.setStatus(HttpStatus.UNAUTHORIZED.value()); + // Use API key to authenticate. This requires you to have an authentication + // provider for API keys. + UserDetails userDetails = userService.loadUserByApiKey(apiKey); + if (userDetails == null) { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.getWriter().write("Invalid API Key."); return; - } - authentication = new ApiKeyAuthenticationToken(userDetails, apiKey, userDetails.getAuthorities()); + } + authentication = + new ApiKeyAuthenticationToken( + userDetails, apiKey, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (AuthenticationException e) { // If API key authentication fails, deny the request @@ -73,32 +72,38 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { // If we still don't have any authentication, deny the request if (authentication == null || !authentication.isAuthenticated()) { - String method = request.getMethod(); - if ("GET".equalsIgnoreCase(method) && !"/login".equals(requestURI)) { - response.sendRedirect("/login"); // redirect to the login page - return; + String method = request.getMethod(); + String contextPath = request.getContextPath(); + + if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { + response.sendRedirect(contextPath + "/login"); // redirect to the login page + return; } else { - 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"); - return; + 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"); + return; } } filterChain.doFilter(request, response); } - + @Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { String uri = request.getRequestURI(); - + String contextPath = request.getContextPath(); String[] permitAllPatterns = { - "/login", - "/register", - "/error", - "/images/", - "/public/", - "/css/", - "/js/" + contextPath + "/login", + contextPath + "/register", + contextPath + "/error", + contextPath + "/images/", + contextPath + "/public/", + contextPath + "/css/", + contextPath + "/js/", + contextPath + "/pdfjs/", + contextPath + "/site.webmanifest" }; for (String pattern : permitAllPatterns) { @@ -109,5 +114,4 @@ public class UserAuthenticationFilter extends OncePerRequestFilter { return false; } - } diff --git a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java index f23e5ce3..6c315971 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserBasedRateLimitingFilter.java @@ -20,28 +20,29 @@ import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bucket; import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.Refill; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.Role; + @Component public class UserBasedRateLimitingFilter extends OncePerRequestFilter { - private final Map apiBuckets = new ConcurrentHashMap<>(); + private final Map apiBuckets = new ConcurrentHashMap<>(); private final Map webBuckets = new ConcurrentHashMap<>(); - @Autowired - private UserDetailsService userDetailsService; + @Autowired private UserDetailsService userDetailsService; @Autowired @Qualifier("rateLimit") public boolean rateLimit; @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { + protected void doFilterInternal( + HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { if (!rateLimit) { // If rateLimit is not enabled, just pass all requests without rate limiting filterChain.doFilter(request, response); @@ -60,7 +61,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { // Check for API key in the request headers String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.trim().isEmpty()) { - identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames + identifier = + "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames } else { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { @@ -74,14 +76,27 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { identifier = request.getRemoteAddr(); } - Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); + Role userRole = + getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication()); if (request.getHeader("X-API-Key") != null) { // It's an API call - processRequest(userRole.getApiCallsPerDay(), identifier, apiBuckets, request, response, filterChain); + processRequest( + userRole.getApiCallsPerDay(), + identifier, + apiBuckets, + request, + response, + filterChain); } else { // It's a Web UI call - processRequest(userRole.getWebCallsPerDay(), identifier, webBuckets, request, response, filterChain); + processRequest( + userRole.getWebCallsPerDay(), + identifier, + webBuckets, + request, + response, + filterChain); } } @@ -98,8 +113,13 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { throw new IllegalStateException("User does not have a valid role."); } - private void processRequest(int limitPerDay, String identifier, Map buckets, - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + private void processRequest( + int limitPerDay, + String identifier, + Map buckets, + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws IOException, ServletException { Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1); @@ -116,10 +136,8 @@ public class UserBasedRateLimitingFilter extends OncePerRequestFilter { } private Bucket createUserBucket(int limitPerDay) { - Bandwidth limit = Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); + Bandwidth limit = + Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1))); return Bucket.builder().addLimit(limit).build(); } } - - - diff --git a/src/main/java/stirling/software/SPDF/config/security/UserService.java b/src/main/java/stirling/software/SPDF/config/security/UserService.java index 46c5aeff..986bb16f 100644 --- a/src/main/java/stirling/software/SPDF/config/security/UserService.java +++ b/src/main/java/stirling/software/SPDF/config/security/UserService.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.config.security; + import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -16,41 +17,40 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.model.Authority; +import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; import stirling.software.SPDF.repository.UserRepository; -@Service -public class UserService { - - @Autowired - private UserRepository userRepository; - @Autowired - private PasswordEncoder passwordEncoder; +@Service +public class UserService implements UserServiceInterface { + + @Autowired private UserRepository userRepository; + + @Autowired private PasswordEncoder passwordEncoder; public Authentication getAuthentication(String apiKey) { User user = getUserByApiKey(apiKey); if (user == null) { throw new UsernameNotFoundException("API key is not valid"); } - + // Convert the user into an Authentication object return new UsernamePasswordAuthenticationToken( - user, // principal (typically the user) - null, // credentials (we don't expose the password or API key here) - getAuthorities(user) // user's authorities (roles/permissions) - ); + user, // principal (typically the user) + null, // credentials (we don't expose the password or API key here) + getAuthorities(user) // user's authorities (roles/permissions) + ); } - + private Collection getAuthorities(User user) { // Convert each Authority object into a SimpleGrantedAuthority object. - return user.getAuthorities().stream() - .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) - .collect(Collectors.toList()); - - + return user.getAuthorities().stream() + .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) + .collect(Collectors.toList()); } - + private String generateApiKey() { String apiKey; do { @@ -60,9 +60,11 @@ public class UserService { } public User addApiKeyToUser(String username) { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); - + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); + user.setApiKey(generateApiKey()); return userRepository.save(user); } @@ -72,8 +74,10 @@ public class UserService { } public String getApiKeyForUser(String username) { - User user = userRepository.findByUsername(username) - .orElseThrow(() -> new UsernameNotFoundException("User not found")); + User user = + userRepository + .findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found")); return user.getApiKey(); } @@ -84,27 +88,25 @@ public class UserService { public User getUserByApiKey(String apiKey) { return userRepository.findByApiKey(apiKey); } - + public UserDetails loadUserByApiKey(String apiKey) { User userOptional = userRepository.findByApiKey(apiKey); if (userOptional != null) { User user = userOptional; // Convert your User entity to a UserDetails object with authorities return new org.springframework.security.core.userdetails.User( - user.getUsername(), - user.getPassword(), // you might not need this for API key auth - getAuthorities(user) - ); + user.getUsername(), + user.getPassword(), // you might not need this for API key auth + getAuthorities(user)); } - return null; // or throw an exception + return null; // or throw an exception } - public boolean validateApiKeyForUser(String username, String apiKey) { Optional userOpt = userRepository.findByUsername(username); return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey); } - + public void saveUser(String username, String password) { User user = new User(); user.setUsername(username); @@ -122,7 +124,7 @@ public class UserService { user.setFirstLogin(firstLogin); userRepository.save(user); } - + public void saveUser(String username, String password, String role) { User user = new User(); user.setUsername(username); @@ -132,37 +134,42 @@ public class UserService { user.setFirstLogin(false); userRepository.save(user); } - + public void deleteUser(String username) { - Optional userOpt = userRepository.findByUsername(username); - if (userOpt.isPresent()) { - userRepository.delete(userOpt.get()); - } + Optional userOpt = userRepository.findByUsername(username); + if (userOpt.isPresent()) { + for (Authority authority : userOpt.get().getAuthorities()) { + if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { + return; + } + } + userRepository.delete(userOpt.get()); + } } - + public boolean usernameExists(String username) { return userRepository.findByUsername(username).isPresent(); } - + public boolean hasUsers() { return userRepository.count() > 0; } - + public void updateUserSettings(String username, Map updates) { Optional userOpt = userRepository.findByUsername(username); if (userOpt.isPresent()) { User user = userOpt.get(); Map settingsMap = user.getSettings(); - if(settingsMap == null) { - settingsMap = new HashMap(); - } + if (settingsMap == null) { + settingsMap = new HashMap(); + } settingsMap.clear(); settingsMap.putAll(updates); user.setSettings(settingsMap); userRepository.save(user); - } + } } public Optional findByUsername(String username) { @@ -178,13 +185,12 @@ public class UserService { user.setPassword(passwordEncoder.encode(newPassword)); userRepository.save(user); } - + public void changeFirstUse(User user, boolean firstUse) { user.setFirstLogin(firstUse); userRepository.save(user); } - - + public boolean isPasswordCorrect(User user, String currentPassword) { return passwordEncoder.matches(currentPassword, user.getPassword()); } diff --git a/src/main/java/stirling/software/SPDF/controller/api/CropController.java b/src/main/java/stirling/software/SPDF/controller/api/CropController.java index d2723cce..a547d89f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/CropController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/CropController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.CropPdfForm; import stirling.software.SPDF.utils.WebResponseUtils; @@ -28,59 +29,62 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class CropController { - private static final Logger logger = LoggerFactory.getLogger(CropController.class); + private static final Logger logger = LoggerFactory.getLogger(CropController.class); - @PostMapping(value = "/crop", consumes = "multipart/form-data") - @Operation(summary = "Crops a PDF document", description = "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") - public ResponseEntity cropPdf(@ModelAttribute CropPdfForm form) - throws IOException { + @PostMapping(value = "/crop", consumes = "multipart/form-data") + @Operation( + summary = "Crops a PDF document", + description = + "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") + public ResponseEntity cropPdf(@ModelAttribute CropPdfForm form) throws IOException { + PDDocument sourceDocument = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); - + PDDocument newDocument = new PDDocument(); -PDDocument sourceDocument = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes())); + int totalPages = sourceDocument.getNumberOfPages(); -PDDocument newDocument = new PDDocument(); + LayerUtility layerUtility = new LayerUtility(newDocument); -int totalPages = sourceDocument.getNumberOfPages(); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); -LayerUtility layerUtility = new LayerUtility(newDocument); + // Create a new page with the size of the source page + PDPage newPage = new PDPage(sourcePage.getMediaBox()); + newDocument.addPage(newPage); + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); -for (int i = 0; i < totalPages; i++) { - PDPage sourcePage = sourceDocument.getPage(i); - - // Create a new page with the size of the source page - PDPage newPage = new PDPage(sourcePage.getMediaBox()); - newDocument.addPage(newPage); - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + // Import the source page as a form XObject + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); - // Import the source page as a form XObject - PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.saveGraphicsState(); - contentStream.saveGraphicsState(); - - // Define the crop area - contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight()); - contentStream.clip(); + // Define the crop area + contentStream.addRect(form.getX(), form.getY(), form.getWidth(), form.getHeight()); + contentStream.clip(); - // Draw the entire formXObject - contentStream.drawForm(formXObject); + // Draw the entire formXObject + contentStream.drawForm(formXObject); - contentStream.restoreGraphicsState(); - - contentStream.close(); - - // Now, set the new page's media box to the cropped size - newPage.setMediaBox(new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight())); -} - -ByteArrayOutputStream baos = new ByteArrayOutputStream(); -newDocument.save(baos); -newDocument.close(); -sourceDocument.close(); - -byte[] pdfContent = baos.toByteArray(); -return WebResponseUtils.bytesToWebResponse(pdfContent, form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_cropped.pdf"); - } + contentStream.restoreGraphicsState(); + contentStream.close(); + + // Now, set the new page's media box to the cropped size + newPage.setMediaBox( + new PDRectangle(form.getX(), form.getY(), form.getWidth(), form.getHeight())); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + sourceDocument.close(); + + byte[] pdfContent = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + pdfContent, + form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_cropped.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java index 7db00a31..70b79191 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MergeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MergeController.java @@ -1,7 +1,15 @@ package stirling.software.SPDF.controller.api; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + import org.apache.pdfbox.io.MemoryUsageSetting; import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.pdmodel.PDDocument; @@ -14,19 +22,13 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.utils.WebResponseUtils; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; - @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") @@ -34,7 +36,6 @@ public class MergeController { private static final Logger logger = LoggerFactory.getLogger(MergeController.class); - private PDDocument mergeDocuments(List documents) throws IOException { PDDocument mergedDoc = new PDDocument(); for (PDDocument doc : documents) { @@ -52,27 +53,39 @@ public class MergeController { case "byDateModified": return (file1, file2) -> { try { - BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); - BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr1 = + Files.readAttributes( + Paths.get(file1.getOriginalFilename()), + BasicFileAttributes.class); + BasicFileAttributes attr2 = + Files.readAttributes( + Paths.get(file2.getOriginalFilename()), + BasicFileAttributes.class); return attr1.lastModifiedTime().compareTo(attr2.lastModifiedTime()); } catch (IOException e) { - return 0; // If there's an error, treat them as equal + return 0; // If there's an error, treat them as equal } }; case "byDateCreated": return (file1, file2) -> { try { - BasicFileAttributes attr1 = Files.readAttributes(Paths.get(file1.getOriginalFilename()), BasicFileAttributes.class); - BasicFileAttributes attr2 = Files.readAttributes(Paths.get(file2.getOriginalFilename()), BasicFileAttributes.class); + BasicFileAttributes attr1 = + Files.readAttributes( + Paths.get(file1.getOriginalFilename()), + BasicFileAttributes.class); + BasicFileAttributes attr2 = + Files.readAttributes( + Paths.get(file2.getOriginalFilename()), + BasicFileAttributes.class); return attr1.creationTime().compareTo(attr2.creationTime()); } catch (IOException e) { - return 0; // If there's an error, treat them as equal + return 0; // If there's an error, treat them as equal } }; case "byPDFTitle": return (file1, file2) -> { try (PDDocument doc1 = PDDocument.load(file1.getInputStream()); - PDDocument doc2 = PDDocument.load(file2.getInputStream())) { + PDDocument doc2 = PDDocument.load(file2.getInputStream())) { String title1 = doc1.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle(); return title1.compareTo(title2); @@ -82,14 +95,17 @@ public class MergeController { }; case "orderProvided": default: - return (file1, file2) -> 0; // Default is the order provided + return (file1, file2) -> 0; // Default is the order provided } } @PostMapping(consumes = "multipart/form-data", value = "/merge-pdfs") - @Operation(summary = "Merge multiple PDF files into one", - description = "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") - public ResponseEntity mergePdfs(@ModelAttribute MergePdfsRequest form) throws IOException { + @Operation( + summary = "Merge multiple PDF files into one", + description = + "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") + public ResponseEntity mergePdfs(@ModelAttribute MergePdfsRequest form) + throws IOException { try { MultipartFile[] files = form.getFileInput(); Arrays.sort(files, getSortComparator(form.getSortType())); @@ -101,14 +117,16 @@ public class MergeController { mergedDoc.addSource(new ByteArrayInputStream(file.getBytes())); } - mergedDoc.setDestinationFileName(files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); + mergedDoc.setDestinationFileName( + files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); mergedDoc.setDestinationStream(docOutputstream); mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly()); - return WebResponseUtils.bytesToWebResponse(docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); + return WebResponseUtils.bytesToWebResponse( + docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); } catch (Exception ex) { logger.error("Error in merge pdf process", ex); - throw ex; + throw ex; } } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java index ebe81ffd..52127571 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/MultiPageLayoutController.java @@ -1,6 +1,5 @@ package stirling.software.SPDF.controller.api; - import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -23,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -31,94 +31,110 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "General", description = "General APIs") public class MultiPageLayoutController { - private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); + private static final Logger logger = LoggerFactory.getLogger(MultiPageLayoutController.class); - @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") - @Operation( - summary = "Merge multiple pages of a PDF document into a single page", - description = "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity mergeMultiplePagesIntoOne(@ModelAttribute MergeMultiplePagesRequest request) - throws IOException { + @PostMapping(value = "/multi-page-layout", consumes = "multipart/form-data") + @Operation( + summary = "Merge multiple pages of a PDF document into a single page", + description = + "This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO") + public ResponseEntity mergeMultiplePagesIntoOne( + @ModelAttribute MergeMultiplePagesRequest request) throws IOException { - int pagesPerSheet = request.getPagesPerSheet(); - MultipartFile file = request.getFileInput(); - boolean addBorder = request.isAddBorder(); - - if (pagesPerSheet != 2 && pagesPerSheet != 3 && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { - throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); - } + int pagesPerSheet = request.getPagesPerSheet(); + MultipartFile file = request.getFileInput(); + boolean addBorder = request.isAddBorder(); - int cols = pagesPerSheet == 2 || pagesPerSheet == 3 ? pagesPerSheet : (int) Math.sqrt(pagesPerSheet); - int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); + if (pagesPerSheet != 2 + && pagesPerSheet != 3 + && pagesPerSheet != (int) Math.sqrt(pagesPerSheet) * Math.sqrt(pagesPerSheet)) { + throw new IllegalArgumentException("pagesPerSheet must be 2, 3 or a perfect square"); + } - PDDocument sourceDocument = PDDocument.load(file.getInputStream()); - PDDocument newDocument = new PDDocument(); - PDPage newPage = new PDPage(PDRectangle.A4); - newDocument.addPage(newPage); + int cols = + pagesPerSheet == 2 || pagesPerSheet == 3 + ? pagesPerSheet + : (int) Math.sqrt(pagesPerSheet); + int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); - int totalPages = sourceDocument.getNumberOfPages(); - float cellWidth = newPage.getMediaBox().getWidth() / cols; - float cellHeight = newPage.getMediaBox().getHeight() / rows; + PDDocument sourceDocument = PDDocument.load(file.getInputStream()); + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(PDRectangle.A4); + newDocument.addPage(newPage); - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); - LayerUtility layerUtility = new LayerUtility(newDocument); + int totalPages = sourceDocument.getNumberOfPages(); + float cellWidth = newPage.getMediaBox().getWidth() / cols; + float cellHeight = newPage.getMediaBox().getHeight() / rows; - float borderThickness = 1.5f; // Specify border thickness as required - contentStream.setLineWidth(borderThickness); - contentStream.setStrokingColor(Color.BLACK); - - for (int i = 0; i < totalPages; i++) { - if (i != 0 && i % pagesPerSheet == 0) { - // Close the current content stream and create a new page and content stream - contentStream.close(); - newPage = new PDPage(PDRectangle.A4); - newDocument.addPage(newPage); - contentStream = new PDPageContentStream(newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); - } + PDPageContentStream contentStream = + new PDPageContentStream( + newDocument, newPage, PDPageContentStream.AppendMode.APPEND, true, true); + LayerUtility layerUtility = new LayerUtility(newDocument); - PDPage sourcePage = sourceDocument.getPage(i); - PDRectangle rect = sourcePage.getMediaBox(); - float scaleWidth = cellWidth / rect.getWidth(); - float scaleHeight = cellHeight / rect.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight); + float borderThickness = 1.5f; // Specify border thickness as required + contentStream.setLineWidth(borderThickness); + contentStream.setStrokingColor(Color.BLACK); - int adjustedPageIndex = i % pagesPerSheet; // This will reset the index for every new page - int rowIndex = adjustedPageIndex / cols; - int colIndex = adjustedPageIndex % cols; + for (int i = 0; i < totalPages; i++) { + if (i != 0 && i % pagesPerSheet == 0) { + // Close the current content stream and create a new page and content stream + contentStream.close(); + newPage = new PDPage(PDRectangle.A4); + newDocument.addPage(newPage); + contentStream = + new PDPageContentStream( + newDocument, + newPage, + PDPageContentStream.AppendMode.APPEND, + true, + true); + } - float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; - float y = newPage.getMediaBox().getHeight() - ((rowIndex + 1) * cellHeight - (cellHeight - rect.getHeight() * scale) / 2); + PDPage sourcePage = sourceDocument.getPage(i); + PDRectangle rect = sourcePage.getMediaBox(); + float scaleWidth = cellWidth / rect.getWidth(); + float scaleHeight = cellHeight / rect.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight); - contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); - contentStream.transform(Matrix.getScaleInstance(scale, scale)); + int adjustedPageIndex = + i % pagesPerSheet; // This will reset the index for every new page + int rowIndex = adjustedPageIndex / cols; + int colIndex = adjustedPageIndex % cols; - PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); - contentStream.drawForm(formXObject); + float x = colIndex * cellWidth + (cellWidth - rect.getWidth() * scale) / 2; + float y = + newPage.getMediaBox().getHeight() + - ((rowIndex + 1) * cellHeight + - (cellHeight - rect.getHeight() * scale) / 2); - contentStream.restoreGraphicsState(); - - if(addBorder) { - // Draw border around each page - float borderX = colIndex * cellWidth; - float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; - contentStream.addRect(borderX, borderY, cellWidth, cellHeight); - contentStream.stroke(); - } - } + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); + PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(formXObject); - contentStream.close(); // Close the final content stream - sourceDocument.close(); + contentStream.restoreGraphicsState(); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - newDocument.close(); + if (addBorder) { + // Draw border around each page + float borderX = colIndex * cellWidth; + float borderY = newPage.getMediaBox().getHeight() - (rowIndex + 1) * cellHeight; + contentStream.addRect(borderX, borderY, cellWidth, cellHeight); + contentStream.stroke(); + } + } - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(result, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); - } + contentStream.close(); // Close the final content stream + sourceDocument.close(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + result, + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java index 1dc81e9f..f6099c3a 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/PdfOverlayController.java @@ -1,8 +1,11 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.pdfbox.multipdf.Overlay; @@ -17,60 +20,87 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class PdfOverlayController { @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") - public ResponseEntity overlayPdfs(@ModelAttribute OverlayPdfsRequest request) throws IOException { + @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 overlayPdfs(@ModelAttribute OverlayPdfsRequest request) + throws IOException { MultipartFile baseFile = request.getFileInput(); int overlayPos = request.getOverlayPosition(); - + MultipartFile[] overlayFiles = request.getOverlayFiles(); File[] overlayPdfFiles = new File[overlayFiles.length]; - 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 - - try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); - Overlay overlay = new Overlay()) { - Map overlayGuide = prepareOverlayGuide(basePdf.getNumberOfPages(), overlayPdfFiles, mode, counts); - - overlay.setInputPDF(basePdf); - if(overlayPos == 0) { - overlay.setOverlayPosition(Overlay.Position.FOREGROUND); - } else { - overlay.setOverlayPosition(Overlay.Position.BACKGROUND); - } - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - overlay.overlay(overlayGuide).save(outputStream); - byte[] data = outputStream.toByteArray(); - String outputFilename = baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"; // Remove file extension and append .pdf - - return WebResponseUtils.bytesToWebResponse(data, outputFilename, MediaType.APPLICATION_PDF); - } + List tempFiles = new ArrayList<>(); // List to keep track of temporary files + + 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 + + try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream()); + Overlay overlay = new Overlay()) { + Map overlayGuide = + prepareOverlayGuide( + basePdf.getNumberOfPages(), + overlayPdfFiles, + mode, + counts, + tempFiles); + + overlay.setInputPDF(basePdf); + if (overlayPos == 0) { + overlay.setOverlayPosition(Overlay.Position.FOREGROUND); + } else { + overlay.setOverlayPosition(Overlay.Position.BACKGROUND); + } + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + overlay.overlay(overlayGuide).save(outputStream); + byte[] data = outputStream.toByteArray(); + String outputFilename = + baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_overlayed.pdf"; // Remove file extension and append .pdf + + return WebResponseUtils.bytesToWebResponse( + data, outputFilename, MediaType.APPLICATION_PDF); + } } finally { 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 prepareOverlayGuide(int basePageCount, File[] overlayFiles, String mode, int[] counts) throws IOException { + private Map prepareOverlayGuide( + int basePageCount, File[] overlayFiles, String mode, int[] counts, List tempFiles) + throws IOException { Map overlayGuide = new HashMap<>(); switch (mode) { case "SequentialOverlay": - sequentialOverlay(overlayGuide, overlayFiles, basePageCount); + sequentialOverlay(overlayGuide, overlayFiles, basePageCount, tempFiles); break; case "InterleavedOverlay": interleavedOverlay(overlayGuide, overlayFiles, basePageCount); @@ -84,42 +114,85 @@ public class PdfOverlayController { return overlayGuide; } - private void sequentialOverlay(Map overlayGuide, File[] overlayFiles, int basePageCount) throws IOException { - if (overlayFiles.length != 1 || basePageCount != PDDocument.load(overlayFiles[0]).getNumberOfPages()) { - throw new IllegalArgumentException("Overlay file count and base page count must match for sequential overlay."); - } + private void sequentialOverlay( + Map overlayGuide, + File[] overlayFiles, + int basePageCount, + List tempFiles) + throws IOException { + int overlayFileIndex = 0; + int pageCountInCurrentOverlay = 0; - File overlayFile = overlayFiles[0]; - try (PDDocument overlayPdf = PDDocument.load(overlayFile)) { - for (int i = 1; i <= overlayPdf.getNumberOfPages(); i++) { - if (i > basePageCount) break; - overlayGuide.put(i, overlayFile.getAbsolutePath()); + for (int basePageIndex = 1; basePageIndex <= basePageCount; basePageIndex++) { + if (pageCountInCurrentOverlay == 0 + || pageCountInCurrentOverlay + >= getNumberOfPages(overlayFiles[overlayFileIndex])) { + pageCountInCurrentOverlay = 0; + overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; + } + + 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 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 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 overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) throws IOException { + private void fixedRepeatOverlay( + Map overlayGuide, File[] overlayFiles, int[] counts, int basePageCount) + throws IOException { 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; for (int i = 0; i < overlayFiles.length; i++) { - File overlayFile = overlayFiles[i]; + File overlayFile = overlayFiles[i]; int repeatCount = counts[i]; - for (int j = 0; j < repeatCount; j++) { - if (currentPage > basePageCount) break; - overlayGuide.put(currentPage++, overlayFile.getAbsolutePath()); + + // Load the overlay document to check its page count + 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. diff --git a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java index b20bd99b..8c31bf3c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RearrangePagesPDFController.java @@ -17,200 +17,204 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.general.RearrangePagesRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class RearrangePagesPDFController { - private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); + private static final Logger logger = LoggerFactory.getLogger(RearrangePagesPDFController.class); - @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") - @Operation(summary = "Remove pages from a PDF file", description = "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") - public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request ) - throws IOException { + @PostMapping(consumes = "multipart/form-data", value = "/remove-pages") + @Operation( + summary = "Remove pages from a PDF file", + description = + "This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO") + public ResponseEntity deletePages(@ModelAttribute PDFWithPageNums request) + throws IOException { - MultipartFile pdfFile = request.getFileInput(); - String pagesToDelete = request.getPageNumbers(); - - PDDocument document = PDDocument.load(pdfFile.getBytes()); + MultipartFile pdfFile = request.getFileInput(); + String pagesToDelete = request.getPageNumbers(); - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pagesToDelete.split(","); + PDDocument document = PDDocument.load(pdfFile.getBytes()); - List pagesToRemove = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pagesToDelete.split(","); - for (int i = pagesToRemove.size() - 1; i >= 0; i--) { - int pageIndex = pagesToRemove.get(i); - document.removePage(pageIndex); - } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); + List pagesToRemove = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); - } - - - - private List removeFirst(int totalPages) { - if (totalPages <= 1) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i <= totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List removeLast(int totalPages) { - if (totalPages <= 1) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 1; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List removeFirstAndLast(int totalPages) { - if (totalPages <= 2) - return new ArrayList<>(); - List newPageOrder = new ArrayList<>(); - for (int i = 2; i < totalPages; i++) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List reverseOrder(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = totalPages; i >= 1; i--) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } - - private List duplexSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages - for (int i = 1; i <= half; i++) { - newPageOrder.add(i - 1); - if (i <= totalPages - half) { // Avoid going out of bounds - newPageOrder.add(totalPages - i); - } - } - return newPageOrder; - } - - private List bookletSort(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < totalPages / 2; i++) { - newPageOrder.add(i); - newPageOrder.add(totalPages - i - 1); - } - return newPageOrder; - } - - private List sideStitchBooklet(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 0; i < (totalPages + 3) / 4; i++) { - int begin = i * 4; - newPageOrder.add(Math.min(begin + 3, totalPages - 1)); - newPageOrder.add(Math.min(begin, totalPages - 1)); - newPageOrder.add(Math.min(begin + 1, totalPages - 1)); - newPageOrder.add(Math.min(begin + 2, totalPages - 1)); - } - return newPageOrder; + for (int i = pagesToRemove.size() - 1; i >= 0; i--) { + int pageIndex = pagesToRemove.get(i); + document.removePage(pageIndex); + } + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf"); } - private List oddEvenSplit(int totalPages) { - List newPageOrder = new ArrayList<>(); - for (int i = 1; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - for (int i = 2; i <= totalPages; i += 2) { - newPageOrder.add(i - 1); - } - return newPageOrder; - } + private List removeFirst(int totalPages) { + if (totalPages <= 1) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i <= totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - private List processSortTypes(String sortTypes, int totalPages) { - try { - SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); - switch (mode) { - case REVERSE_ORDER: - return reverseOrder(totalPages); - case DUPLEX_SORT: - return duplexSort(totalPages); - case BOOKLET_SORT: - return bookletSort(totalPages); - case SIDE_STITCH_BOOKLET_SORT: - return sideStitchBooklet(totalPages); - case ODD_EVEN_SPLIT: - return oddEvenSplit(totalPages); - case REMOVE_FIRST: - return removeFirst(totalPages); - case REMOVE_LAST: - return removeLast(totalPages); - case REMOVE_FIRST_AND_LAST: - return removeFirstAndLast(totalPages); - default: - throw new IllegalArgumentException("Unsupported custom mode"); - } - } catch (IllegalArgumentException e) { - logger.error("Unsupported custom mode", e); - return null; - } - } + private List removeLast(int totalPages) { + if (totalPages <= 1) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 1; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") - @Operation(summary = "Rearrange pages in a PDF file", description = "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") - public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) throws IOException { - MultipartFile pdfFile = request.getFileInput(); - String pageOrder = request.getPageNumbers(); - String sortType = request.getCustomMode(); - try { - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); + private List removeFirstAndLast(int totalPages) { + if (totalPages <= 2) return new ArrayList<>(); + List newPageOrder = new ArrayList<>(); + for (int i = 2; i < totalPages; i++) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - // Split the page order string into an array of page numbers or range of numbers - String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; - int totalPages = document.getNumberOfPages(); - List newPageOrder; - if (sortType != null && sortType.length() > 0) { - newPageOrder = processSortTypes(sortType, totalPages); - } else { - newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); - } - logger.info("newPageOrder = " +newPageOrder); - logger.info("totalPages = " +totalPages); - // Create a new list to hold the pages in the new order - List newPages = new ArrayList<>(); - for (int i = 0; i < newPageOrder.size(); i++) { - newPages.add(document.getPage(newPageOrder.get(i))); - } + private List reverseOrder(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = totalPages; i >= 1; i--) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } - // Remove all the pages from the original document - for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { - document.removePage(i); - } + private List duplexSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + int half = (totalPages + 1) / 2; // This ensures proper behavior with odd numbers of pages + for (int i = 1; i <= half; i++) { + newPageOrder.add(i - 1); + if (i <= totalPages - half) { // Avoid going out of bounds + newPageOrder.add(totalPages - i); + } + } + return newPageOrder; + } - // Add the pages in the new order - for (PDPage page : newPages) { - document.addPage(page); - } + private List bookletSort(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 0; i < totalPages / 2; i++) { + newPageOrder.add(i); + newPageOrder.add(totalPages - i - 1); + } + return newPageOrder; + } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rearranged.pdf"); - } catch (IOException e) { - logger.error("Failed rearranging documents", e); - return null; - } - } + private List sideStitchBooklet(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 0; i < (totalPages + 3) / 4; i++) { + int begin = i * 4; + newPageOrder.add(Math.min(begin + 3, totalPages - 1)); + newPageOrder.add(Math.min(begin, totalPages - 1)); + newPageOrder.add(Math.min(begin + 1, totalPages - 1)); + newPageOrder.add(Math.min(begin + 2, totalPages - 1)); + } + return newPageOrder; + } - + private List oddEvenSplit(int totalPages) { + List newPageOrder = new ArrayList<>(); + for (int i = 1; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + for (int i = 2; i <= totalPages; i += 2) { + newPageOrder.add(i - 1); + } + return newPageOrder; + } + private List processSortTypes(String sortTypes, int totalPages) { + try { + SortTypes mode = SortTypes.valueOf(sortTypes.toUpperCase()); + switch (mode) { + case REVERSE_ORDER: + return reverseOrder(totalPages); + case DUPLEX_SORT: + return duplexSort(totalPages); + case BOOKLET_SORT: + return bookletSort(totalPages); + case SIDE_STITCH_BOOKLET_SORT: + return sideStitchBooklet(totalPages); + case ODD_EVEN_SPLIT: + return oddEvenSplit(totalPages); + case REMOVE_FIRST: + return removeFirst(totalPages); + case REMOVE_LAST: + return removeLast(totalPages); + case REMOVE_FIRST_AND_LAST: + return removeFirstAndLast(totalPages); + default: + throw new IllegalArgumentException("Unsupported custom mode"); + } + } catch (IllegalArgumentException e) { + logger.error("Unsupported custom mode", e); + return null; + } + } + + @PostMapping(consumes = "multipart/form-data", value = "/rearrange-pages") + @Operation( + summary = "Rearrange pages in a PDF file", + description = + "This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF") + public ResponseEntity rearrangePages(@ModelAttribute RearrangePagesRequest request) + throws IOException { + MultipartFile pdfFile = request.getFileInput(); + String pageOrder = request.getPageNumbers(); + String sortType = request.getCustomMode(); + try { + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); + + // Split the page order string into an array of page numbers or range of numbers + String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; + int totalPages = document.getNumberOfPages(); + List newPageOrder; + if (sortType != null && sortType.length() > 0) { + newPageOrder = processSortTypes(sortType, totalPages); + } else { + newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages); + } + logger.info("newPageOrder = " + newPageOrder); + logger.info("totalPages = " + totalPages); + // Create a new list to hold the pages in the new order + List newPages = new ArrayList<>(); + for (int i = 0; i < newPageOrder.size(); i++) { + newPages.add(document.getPage(newPageOrder.get(i))); + } + + // Remove all the pages from the original document + for (int i = document.getNumberOfPages() - 1; i >= 0; i--) { + document.removePage(i); + } + + // Add the pages in the new order + for (PDPage page : newPages) { + document.addPage(page); + } + + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_rearranged.pdf"); + } catch (IOException e) { + logger.error("Failed rearranging documents", e); + return null; + } + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java index ed527549..883beb5d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/RotationController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/RotationController.java @@ -16,6 +16,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.RotatePDFRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -28,11 +29,11 @@ public class RotationController { @PostMapping(consumes = "multipart/form-data", value = "/rotate-pdf") @Operation( - summary = "Rotate a PDF file", - description = "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity rotatePDF( - @ModelAttribute RotatePDFRequest request) throws IOException { + summary = "Rotate a PDF file", + description = + "This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO") + public ResponseEntity rotatePDF(@ModelAttribute RotatePDFRequest request) + throws IOException { MultipartFile pdfFile = request.getFileInput(); Integer angle = request.getAngle(); // Load the PDF document @@ -45,8 +46,8 @@ public class RotationController { page.setRotation(page.getRotation() + angle); } - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); - + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_rotated.pdf"); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java index 743ea6b1..0dcec05c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ScalePagesController.java @@ -23,88 +23,90 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.ScalePagesRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") public class ScalePagesController { - private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); + private static final Logger logger = LoggerFactory.getLogger(ScalePagesController.class); - @PostMapping(value = "/scale-pages", consumes = "multipart/form-data") - @Operation(summary = "Change the size of a PDF page/document", description = "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") - public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) throws IOException { - MultipartFile file = request.getFileInput(); - String targetPDRectangle = request.getPageSize(); - float scaleFactor = request.getScaleFactor(); + @PostMapping(value = "/scale-pages", consumes = "multipart/form-data") + @Operation( + summary = "Change the size of a PDF page/document", + description = + "This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO") + public ResponseEntity scalePages(@ModelAttribute ScalePagesRequest request) + throws IOException { + MultipartFile file = request.getFileInput(); + String targetPDRectangle = request.getPageSize(); + float scaleFactor = request.getScaleFactor(); - Map sizeMap = new HashMap<>(); - // Add A0 - A10 - sizeMap.put("A0", PDRectangle.A0); - sizeMap.put("A1", PDRectangle.A1); - sizeMap.put("A2", PDRectangle.A2); - sizeMap.put("A3", PDRectangle.A3); - sizeMap.put("A4", PDRectangle.A4); - sizeMap.put("A5", PDRectangle.A5); - sizeMap.put("A6", PDRectangle.A6); + Map sizeMap = new HashMap<>(); + // Add A0 - A10 + sizeMap.put("A0", PDRectangle.A0); + sizeMap.put("A1", PDRectangle.A1); + sizeMap.put("A2", PDRectangle.A2); + sizeMap.put("A3", PDRectangle.A3); + sizeMap.put("A4", PDRectangle.A4); + sizeMap.put("A5", PDRectangle.A5); + sizeMap.put("A6", PDRectangle.A6); - // Add other sizes - sizeMap.put("LETTER", PDRectangle.LETTER); - sizeMap.put("LEGAL", PDRectangle.LEGAL); + // Add other sizes + sizeMap.put("LETTER", PDRectangle.LETTER); + sizeMap.put("LEGAL", PDRectangle.LEGAL); - if (!sizeMap.containsKey(targetPDRectangle)) { - throw new IllegalArgumentException( - "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); - } + if (!sizeMap.containsKey(targetPDRectangle)) { + throw new IllegalArgumentException( + "Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10"); + } - PDRectangle targetSize = sizeMap.get(targetPDRectangle); + PDRectangle targetSize = sizeMap.get(targetPDRectangle); - PDDocument sourceDocument = PDDocument.load(file.getBytes()); - PDDocument outputDocument = new PDDocument(); + PDDocument sourceDocument = PDDocument.load(file.getBytes()); + PDDocument outputDocument = new PDDocument(); - int totalPages = sourceDocument.getNumberOfPages(); - for (int i = 0; i < totalPages; i++) { - PDPage sourcePage = sourceDocument.getPage(i); - PDRectangle sourceSize = sourcePage.getMediaBox(); - - float scaleWidth = targetSize.getWidth() / sourceSize.getWidth(); - float scaleHeight = targetSize.getHeight() / sourceSize.getHeight(); - float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; - - PDPage newPage = new PDPage(targetSize); - outputDocument.addPage(newPage); - - PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true); - - float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2; - float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; - - contentStream.saveGraphicsState(); - contentStream.transform(Matrix.getTranslateInstance(x, y)); - contentStream.transform(Matrix.getScaleInstance(scale, scale)); - - LayerUtility layerUtility = new LayerUtility(outputDocument); - PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i); - contentStream.drawForm(form); + int totalPages = sourceDocument.getNumberOfPages(); + for (int i = 0; i < totalPages; i++) { + PDPage sourcePage = sourceDocument.getPage(i); + PDRectangle sourceSize = sourcePage.getMediaBox(); - contentStream.restoreGraphicsState(); - contentStream.close(); - } + float scaleWidth = targetSize.getWidth() / sourceSize.getWidth(); + float scaleHeight = targetSize.getHeight() / sourceSize.getHeight(); + float scale = Math.min(scaleWidth, scaleHeight) * scaleFactor; + PDPage newPage = new PDPage(targetSize); + outputDocument.addPage(newPage); + PDPageContentStream contentStream = + new PDPageContentStream( + outputDocument, newPage, PDPageContentStream.AppendMode.APPEND, true); + float x = (targetSize.getWidth() - sourceSize.getWidth() * scale) / 2; + float y = (targetSize.getHeight() - sourceSize.getHeight() * scale) / 2; + contentStream.saveGraphicsState(); + contentStream.transform(Matrix.getTranslateInstance(x, y)); + contentStream.transform(Matrix.getScaleInstance(scale, scale)); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - outputDocument.save(baos); - outputDocument.close(); - sourceDocument.close(); - - - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), - file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); - } + LayerUtility layerUtility = new LayerUtility(outputDocument); + PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, i); + contentStream.drawForm(form); + contentStream.restoreGraphicsState(); + contentStream.close(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + outputDocument.save(baos); + outputDocument.close(); + sourceDocument.close(); + + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scaled.pdf"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java index 0651949e..a521769e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPDFController.java @@ -25,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.utils.WebResponseUtils; @@ -36,19 +37,24 @@ public class SplitPDFController { private static final Logger logger = LoggerFactory.getLogger(SplitPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/split-pages") - @Operation(summary = "Split a PDF file into separate documents", - description = "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") - public ResponseEntity splitPdf(@ModelAttribute PDFWithPageNums request) throws IOException { - MultipartFile file = request.getFileInput(); + @Operation( + summary = "Split a PDF file into separate documents", + description = + "This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO") + public ResponseEntity splitPdf(@ModelAttribute PDFWithPageNums request) + throws IOException { + MultipartFile file = request.getFileInput(); String pages = request.getPageNumbers(); // open the pdf document InputStream inputStream = file.getInputStream(); PDDocument document = PDDocument.load(inputStream); List pageNumbers = request.getPageNumbersList(document); - if(!pageNumbers.contains(document.getNumberOfPages() - 1)) - pageNumbers.add(document.getNumberOfPages()- 1); - logger.info("Splitting PDF into pages: {}", pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); + if (!pageNumbers.contains(document.getNumberOfPages() - 1)) + pageNumbers.add(document.getNumberOfPages() - 1); + logger.info( + "Splitting PDF into pages: {}", + pageNumbers.stream().map(String::valueOf).collect(Collectors.joining(","))); // split the document List splitDocumentsBoas = new ArrayList<>(); @@ -72,7 +78,6 @@ public class SplitPDFController { } } - // closing the original document document.close(); @@ -104,8 +109,7 @@ public class SplitPDFController { Files.delete(zipFile); // return the Resource in the response - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); - + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java index 29a2f426..5d96920f 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySectionsController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -25,17 +26,22 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") -@Tag(name = "Misc", description = "Miscellaneous APIs") +@Tag(name = "General", description = "General APIs") public class SplitPdfBySectionsController { - - @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.") - public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) throws Exception { + @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 Output:ZIP-PDF Type:SISO") + public ResponseEntity splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) + throws Exception { List splitDocumentsBoas = new ArrayList<>(); MultipartFile file = request.getFileInput(); @@ -59,8 +65,6 @@ public class SplitPdfBySectionsController { String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); byte[] data; - - try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { int pageNum = 1; for (int i = 0; i < splitDocumentsBoas.size(); i++) { @@ -82,10 +86,13 @@ public class SplitPdfBySectionsController { Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + "_split.zip", MediaType.APPLICATION_OCTET_STREAM); } - - public List splitPdfPages(PDDocument document, int horizontalDivisions, int verticalDivisions) throws IOException { + + public List splitPdfPages( + PDDocument document, int horizontalDivisions, int verticalDivisions) + throws IOException { List splitDocuments = new ArrayList<>(); for (PDPage originalPage : document.getPages()) { @@ -103,9 +110,12 @@ public class SplitPdfBySectionsController { PDPage subPage = new PDPage(new PDRectangle(subPageWidth, subPageHeight)); subDoc.addPage(subPage); - PDFormXObject form = layerUtility.importPageAsForm(document, document.getPages().indexOf(originalPage)); + PDFormXObject form = + layerUtility.importPageAsForm( + document, document.getPages().indexOf(originalPage)); - try (PDPageContentStream contentStream = new PDPageContentStream(subDoc, subPage)) { + try (PDPageContentStream contentStream = + new PDPageContentStream(subDoc, subPage)) { // Set clipping area and position float translateX = -subPageWidth * i; float translateY = height - subPageHeight * (verticalDivisions - j); @@ -127,9 +137,4 @@ public class SplitPdfBySectionsController { return splitDocuments; } - - - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java index 83e7b723..28ac4673 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/SplitPdfBySizeController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api; + import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; @@ -20,31 +21,33 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @RequestMapping("/api/v1/general") -@Tag(name = "Misc", description = "Miscellaneous APIs") +@Tag(name = "General", description = "General APIs") public class SplitPdfBySizeController { - @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" - + " 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") - public ResponseEntity autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) throws Exception { - List splitDocumentsBoas = new ArrayList(); - - - + @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-PDF Type:SISO") + public ResponseEntity autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request) + throws Exception { + List splitDocumentsBoas = new ArrayList(); + MultipartFile file = request.getFileInput(); PDDocument sourceDocument = PDDocument.load(file.getInputStream()); - - //0 = size, 1 = page count, 2 = doc count + + // 0 = size, 1 = page count, 2 = doc count int type = request.getSplitType(); String value = request.getSplitValue(); - + if (type == 0) { // Split by size long maxBytes = GeneralUtils.convertSizeToBytes(value); long currentSize = 0; @@ -93,7 +96,7 @@ public class SplitPdfBySizeController { splitDocumentsBoas.add(currentDocToByteArray(currentDoc)); } } else if (type == 2) { // Split by doc count - int documentCount = Integer.parseInt(value); + int documentCount = Integer.parseInt(value); int totalPageCount = sourceDocument.getNumberOfPages(); int pagesPerDocument = totalPageCount / documentCount; int extraPages = totalPageCount % documentCount; @@ -114,9 +117,7 @@ public class SplitPdfBySizeController { } sourceDocument.close(); - - - + Path zipFile = Files.createTempFile("split_documents", ".zip"); String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); byte[] data; @@ -135,19 +136,18 @@ public class SplitPdfBySizeController { } catch (Exception e) { e.printStackTrace(); } finally { - data = Files.readAllBytes(zipFile); + data = Files.readAllBytes(zipFile); Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - + private ByteArrayOutputStream currentDocToByteArray(PDDocument document) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); document.close(); return baos; } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java index 22bf1d70..cd971b55 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/ToSinglePageController.java @@ -20,8 +20,10 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/general") @Tag(name = "General", description = "General APIs") @@ -29,58 +31,61 @@ public class ToSinglePageController { private static final Logger logger = LoggerFactory.getLogger(ToSinglePageController.class); - @PostMapping(consumes = "multipart/form-data", value = "/pdf-to-single-page") @Operation( - summary = "Convert a multi-page PDF into a single long page PDF", - description = "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) throws IOException { + summary = "Convert a multi-page PDF into a single long page PDF", + description = + "This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO") + public ResponseEntity pdfToSinglePage(@ModelAttribute PDFFile request) + throws IOException { - // Load the source document - PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); + // Load the source document + PDDocument sourceDocument = PDDocument.load(request.getFileInput().getInputStream()); - // Calculate total height and max width - float totalHeight = 0; - float maxWidth = 0; - for (PDPage page : sourceDocument.getPages()) { - PDRectangle pageSize = page.getMediaBox(); - totalHeight += pageSize.getHeight(); - maxWidth = Math.max(maxWidth, pageSize.getWidth()); - } + // Calculate total height and max width + float totalHeight = 0; + float maxWidth = 0; + for (PDPage page : sourceDocument.getPages()) { + PDRectangle pageSize = page.getMediaBox(); + totalHeight += pageSize.getHeight(); + maxWidth = Math.max(maxWidth, pageSize.getWidth()); + } - // Create new document and page with calculated dimensions - PDDocument newDocument = new PDDocument(); - PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); - newDocument.addPage(newPage); + // Create new document and page with calculated dimensions + PDDocument newDocument = new PDDocument(); + PDPage newPage = new PDPage(new PDRectangle(maxWidth, totalHeight)); + newDocument.addPage(newPage); - // Initialize the content stream of the new page - PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); - contentStream.close(); - - LayerUtility layerUtility = new LayerUtility(newDocument); - float yOffset = totalHeight; + // Initialize the content stream of the new page + PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage); + contentStream.close(); - // For each page, copy its content to the new page at the correct offset - for (PDPage page : sourceDocument.getPages()) { - PDFormXObject form = layerUtility.importPageAsForm(sourceDocument, sourceDocument.getPages().indexOf(page)); - AffineTransform af = AffineTransform.getTranslateInstance(0, yOffset - page.getMediaBox().getHeight()); - layerUtility.wrapInSaveRestore(newPage); - String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page); - layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); - yOffset -= page.getMediaBox().getHeight(); - } + LayerUtility layerUtility = new LayerUtility(newDocument); + float yOffset = totalHeight; - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - newDocument.save(baos); - newDocument.close(); - sourceDocument.close(); + // For each page, copy its content to the new page at the correct offset + for (PDPage page : sourceDocument.getPages()) { + PDFormXObject form = + layerUtility.importPageAsForm( + sourceDocument, sourceDocument.getPages().indexOf(page)); + AffineTransform af = + AffineTransform.getTranslateInstance( + 0, yOffset - page.getMediaBox().getHeight()); + layerUtility.wrapInSaveRestore(newPage); + String defaultLayerName = "Layer" + sourceDocument.getPages().indexOf(page); + layerUtility.appendFormAsLayer(newPage, form, af, defaultLayerName); + yOffset -= page.getMediaBox().getHeight(); + } - byte[] result = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(result, request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_singlePage.pdf"); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + newDocument.save(baos); + newDocument.close(); + sourceDocument.close(); - - - + byte[] result = baos.toByteArray(); + return WebResponseUtils.bytesToWebResponse( + result, + request.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_singlePage.pdf"); } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/UserController.java b/src/main/java/stirling/software/SPDF/controller/api/UserController.java index bf451567..89e81c99 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -23,18 +23,20 @@ import org.springframework.web.servlet.view.RedirectView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.config.security.UserService; +import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.User; @Controller @RequestMapping("/api/v1/user") public class UserController { - - @Autowired - private UserService userService; - + + @Autowired private UserService userService; + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/register") - public String register(@RequestParam String username, @RequestParam String password, Model model) { - if(userService.usernameExists(username)) { + public String register( + @RequestParam String username, @RequestParam String password, Model model) { + if (userService.usernameExists(username)) { model.addAttribute("error", "Username already exists"); return "register"; } @@ -42,38 +44,41 @@ public class UserController { userService.saveUser(username, password); return "redirect:/login?registered=true"; } - + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username-and-password") - public RedirectView changeUsernameAndPassword(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newUsername, - @RequestParam String newPassword, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/change-creds?messageType=notAuthenticated"); - } + public RedirectView changeUsernameAndPassword( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/change-creds?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/change-creds?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/change-creds?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/change-creds?messageType=incorrectPassword"); - } - - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - return new RedirectView("/change-creds?messageType=usernameExists"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/change-creds?messageType=incorrectPassword"); + } + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/change-creds?messageType=usernameExists"); + } userService.changePassword(user, newPassword); - if(newUsername != null && newUsername.length() > 0 && !user.getUsername().equals(newUsername)) { + if (newUsername != null + && newUsername.length() > 0 + && !user.getUsername().equals(newUsername)) { userService.changeUsername(user, newUsername); } userService.changeFirstUse(user, false); @@ -84,36 +89,36 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-username") - public RedirectView changeUsername(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newUsername, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/account?messageType=notAuthenticated"); - } + public RedirectView changeUsername( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newUsername, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/account?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/account?messageType=incorrectPassword"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } - if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { - return new RedirectView("/account?messageType=usernameExists"); - } + if (!user.getUsername().equals(newUsername) && userService.usernameExists(newUsername)) { + return new RedirectView("/account?messageType=usernameExists"); + } - if(newUsername != null && newUsername.length() > 0) { + if (newUsername != null && newUsername.length() > 0) { userService.changeUsername(user, newUsername); } @@ -123,28 +128,30 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/change-password") - public RedirectView changePassword(Principal principal, - @RequestParam String currentPassword, - @RequestParam String newPassword, - HttpServletRequest request, - HttpServletResponse response, - RedirectAttributes redirectAttributes) { - if (principal == null) { - return new RedirectView("/account?messageType=notAuthenticated"); - } + public RedirectView changePassword( + Principal principal, + @RequestParam String currentPassword, + @RequestParam String newPassword, + HttpServletRequest request, + HttpServletResponse response, + RedirectAttributes redirectAttributes) { + if (principal == null) { + return new RedirectView("/account?messageType=notAuthenticated"); + } - Optional userOpt = userService.findByUsername(principal.getName()); + Optional userOpt = userService.findByUsername(principal.getName()); - if (userOpt == null || userOpt.isEmpty()) { - return new RedirectView("/account?messageType=userNotFound"); - } + if (userOpt == null || userOpt.isEmpty()) { + return new RedirectView("/account?messageType=userNotFound"); + } - User user = userOpt.get(); + User user = userOpt.get(); - if (!userService.isPasswordCorrect(user, currentPassword)) { - return new RedirectView("/account?messageType=incorrectPassword"); - } + if (!userService.isPasswordCorrect(user, currentPassword)) { + return new RedirectView("/account?messageType=incorrectPassword"); + } userService.changePassword(user, newPassword); @@ -154,55 +161,71 @@ public class UserController { return new RedirectView("/login?messageType=credsUpdated"); } - + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/updateUserSettings") - public String updateUserSettings(HttpServletRequest request, Principal principal) { - Map paramMap = request.getParameterMap(); - Map updates = new HashMap<>(); + public String updateUserSettings(HttpServletRequest request, Principal principal) { + Map paramMap = request.getParameterMap(); + Map updates = new HashMap<>(); - System.out.println("Received parameter map: " + paramMap); + System.out.println("Received parameter map: " + paramMap); - for (Map.Entry entry : paramMap.entrySet()) { - updates.put(entry.getKey(), entry.getValue()[0]); - } + for (Map.Entry entry : paramMap.entrySet()) { + updates.put(entry.getKey(), entry.getValue()[0]); + } - System.out.println("Processed updates: " + updates); + System.out.println("Processed updates: " + updates); - // Assuming you have a method in userService to update the settings for a user - userService.updateUserSettings(principal.getName(), updates); + // Assuming you have a method in userService to update the settings for a user + userService.updateUserSettings(principal.getName(), updates); - return "redirect:/account"; // Redirect to a page of your choice after updating - } + return "redirect:/account"; // Redirect to a page of your choice after updating + } @PreAuthorize("hasRole('ROLE_ADMIN')") @PostMapping("/admin/saveUser") - public RedirectView saveUser(@RequestParam String username, @RequestParam String password, @RequestParam String role, - @RequestParam(name = "forceChange", required = false, defaultValue = "false") boolean forceChange) { - - if(userService.usernameExists(username)) { - return new RedirectView("/addUsers?messageType=usernameExists"); - } + public RedirectView saveUser( + @RequestParam String username, + @RequestParam String password, + @RequestParam String role, + @RequestParam(name = "forceChange", required = false, defaultValue = "false") + boolean forceChange) { + + if (userService.usernameExists(username)) { + 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); - return new RedirectView("/addUsers"); // Redirect to account page after adding the user + return new RedirectView("/addUsers"); // Redirect to account page after adding the user } - @PreAuthorize("hasRole('ROLE_ADMIN')") @PostMapping("/admin/deleteUser/{username}") - public String deleteUser(@PathVariable String username, Authentication authentication) { - - // Get the currently authenticated username + public String deleteUser(@PathVariable String username, Authentication authentication) { + + // Get the currently authenticated username String currentUsername = authentication.getName(); // Check if the provided username matches the current session's username if (currentUsername.equals(username)) { throw new IllegalArgumentException("Cannot delete currently logined in user."); } - - userService.deleteUser(username); + + userService.deleteUser(username); return "redirect:/addUsers"; } - + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/get-api-key") public ResponseEntity getApiKey(Principal principal) { if (principal == null) { @@ -216,6 +239,7 @@ public class UserController { return ResponseEntity.ok(apiKey); } + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PostMapping("/update-api-key") public ResponseEntity updateApiKey(Principal principal) { if (principal == null) { @@ -229,6 +253,4 @@ public class UserController { } return ResponseEntity.ok(apiKey); } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java deleted file mode 100644 index e821a36a..00000000 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertEpubToPdf.java +++ /dev/null @@ -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 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 epubContents = extractEpubContent(fileInput); - List htmlFilesOrder = getHtmlFilesOrderFromOpf(epubContents); - - List 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 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 extractEpubContent(MultipartFile fileInput) throws IOException { - Map 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 getHtmlFilesOrderFromOpf(Map 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 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; - } - - -} diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java index 5839dd2d..bec09040 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertHtmlToPDF.java @@ -9,6 +9,7 @@ import org.springframework.web.multipart.MultipartFile; 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; @@ -18,35 +19,30 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RequestMapping("/api/v1/convert") public class ConvertHtmlToPDF { + @PostMapping(consumes = "multipart/form-data", value = "/html/pdf") + @Operation( + summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", + description = + "This endpoint takes an HTML or ZIP file input and converts it to a PDF format.") + public ResponseEntity HtmlToPdf(@ModelAttribute GeneralFile request) throws Exception { + MultipartFile fileInput = request.getFileInput(); - @PostMapping(consumes = "multipart/form-data", value = "/html/pdf") - @Operation( - summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", - description = "This endpoint takes an HTML or ZIP file input and converts it to a PDF format." - ) - public ResponseEntity HtmlToPdf( - @ModelAttribute GeneralFile request) - throws Exception { - MultipartFile fileInput = request.getFileInput(); + if (fileInput == null) { + throw new IllegalArgumentException( + "Please provide an HTML or ZIP file for conversion."); + } - if (fileInput == null) { - throw new IllegalArgumentException("Please provide an HTML or ZIP file for conversion."); - } + String originalFilename = fileInput.getOriginalFilename(); + if (originalFilename == null + || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { + throw new IllegalArgumentException("File must be either .html or .zip format."); + } + byte[] pdfBytes = FileToPdf.convertHtmlToPdf(fileInput.getBytes(), originalFilename); - String originalFilename = fileInput.getOriginalFilename(); - if (originalFilename == null || (!originalFilename.endsWith(".html") && !originalFilename.endsWith(".zip"))) { - throw new IllegalArgumentException("File must be either .html or .zip format."); - }byte[] pdfBytes = FileToPdf.convertHtmlToPdf( fileInput.getBytes(), originalFilename); - - String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf - - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } - - - - - - + String outputFilename = + originalFilename.replaceFirst("[.][^.]+$", "") + + ".pdf"; // Remove file extension and append .pdf + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java index f0c60b69..a3ea2841 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertImgPDFController.java @@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.converters.ConvertToImageRequest; import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; import stirling.software.SPDF.utils.PdfUtils; @@ -33,15 +34,18 @@ public class ConvertImgPDFController { private static final Logger logger = LoggerFactory.getLogger(ConvertImgPDFController.class); @PostMapping(consumes = "multipart/form-data", value = "/pdf/img") - @Operation(summary = "Convert PDF to image(s)", - description = "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") - public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) throws IOException { + @Operation( + summary = "Convert PDF to image(s)", + description = + "This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional") + public ResponseEntity convertToImage(@ModelAttribute ConvertToImageRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String imageFormat = request.getImageFormat(); String singleOrMultiple = request.getSingleOrMultiple(); String colorType = request.getColorType(); String dpi = request.getDpi(); - + byte[] pdfBytes = file.getBytes(); ImageType colorTypeResult = ImageType.RGB; if ("greyscale".equals(colorType)) { @@ -54,7 +58,14 @@ public class ConvertImgPDFController { byte[] result = null; String filename = file.getOriginalFilename().replaceFirst("[.][^.]+$", ""); try { - result = PdfUtils.convertFromPdf(pdfBytes, imageFormat.toUpperCase(), colorTypeResult, singleImage, Integer.valueOf(dpi), filename); + result = + PdfUtils.convertFromPdf( + pdfBytes, + imageFormat.toUpperCase(), + colorTypeResult, + singleImage, + Integer.valueOf(dpi), + filename); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -65,29 +76,39 @@ public class ConvertImgPDFController { if (singleImage) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.parseMediaType(getMediaType(imageFormat))); - ResponseEntity response = new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); + ResponseEntity response = + new ResponseEntity<>(new ByteArrayResource(result), headers, HttpStatus.OK); return response; } else { ByteArrayResource resource = new ByteArrayResource(result); // return the Resource in the response return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename + "_convertedToImages.zip") - .contentType(MediaType.APPLICATION_OCTET_STREAM).contentLength(resource.contentLength()).body(resource); + .header( + HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=" + filename + "_convertedToImages.zip") + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(resource.contentLength()) + .body(resource); } } @PostMapping(consumes = "multipart/form-data", value = "/img/pdf") - @Operation(summary = "Convert images to a PDF file", - description = "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") - public ResponseEntity convertToPdf(@ModelAttribute ConvertToPdfRequest request) throws IOException { + @Operation( + summary = "Convert images to a PDF file", + description = + "This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:SISO?") + public ResponseEntity convertToPdf(@ModelAttribute ConvertToPdfRequest request) + throws IOException { MultipartFile[] file = request.getFileInput(); String fitOption = request.getFitOption(); String colorType = request.getColorType(); boolean autoRotate = request.isAutoRotate(); - + // Convert the file to PDF and get the resulting bytes byte[] bytes = PdfUtils.imageToPdf(file, fitOption, autoRotate, colorType); - return WebResponseUtils.bytesToWebResponse(bytes, file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); + return WebResponseUtils.bytesToWebResponse( + bytes, + file[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_converted.pdf"); } private String getMediaType(String imageFormat) { diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java index 4191ecdf..8bdc5049 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertMarkdownToPdf.java @@ -12,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile; 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; @@ -20,17 +21,16 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Convert", description = "Convert APIs") @RequestMapping("/api/v1/convert") public class ConvertMarkdownToPdf { - - @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") + + @PostMapping(consumes = "multipart/form-data", value = "/markdown/pdf") @Operation( - summary = "Convert a Markdown file to PDF", - description = "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format." - ) - public ResponseEntity markdownToPdf( - @ModelAttribute GeneralFile request) - throws Exception { - MultipartFile fileInput = request.getFileInput(); - + summary = "Convert a Markdown file to PDF", + description = + "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format.") + public ResponseEntity markdownToPdf(@ModelAttribute GeneralFile request) + throws Exception { + MultipartFile fileInput = request.getFileInput(); + if (fileInput == null) { throw new IllegalArgumentException("Please provide a Markdown file for conversion."); } @@ -45,10 +45,12 @@ public class ConvertMarkdownToPdf { Node document = parser.parse(new String(fileInput.getBytes())); HtmlRenderer renderer = HtmlRenderer.builder().build(); String htmlContent = renderer.render(document); - - byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html"); - String outputFilename = originalFilename.replaceFirst("[.][^.]+$", "") + ".pdf"; // Remove file extension and append .pdf + byte[] pdfBytes = FileToPdf.convertHtmlToPdf(htmlContent.getBytes(), "converted.html"); + + String outputFilename = + originalFilename.replaceFirst("[.][^.]+$", "") + + ".pdf"; // Remove file extension and append .pdf return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java index e1c18a49..c0008046 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertOfficeController.java @@ -18,6 +18,7 @@ import org.springframework.web.multipart.MultipartFile; 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.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -31,20 +32,33 @@ public class ConvertOfficeController { public byte[] convertToPdf(MultipartFile inputFile) throws IOException, InterruptedException { // Check for valid file extension String originalFilename = inputFile.getOriginalFilename(); - if (originalFilename == null || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { + if (originalFilename == null + || !isValidFileExtension(FilenameUtils.getExtension(originalFilename))) { throw new IllegalArgumentException("Invalid file extension"); } // Save the uploaded file to a temporary location - Path tempInputFile = Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); + Path tempInputFile = + Files.createTempFile("input_", "." + FilenameUtils.getExtension(originalFilename)); Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); // Prepare the output file path Path tempOutputFile = Files.createTempFile("output_", ".pdf"); // Run the LibreOffice command - List command = new ArrayList<>(Arrays.asList("unoconv", "-vvv", "-f", "pdf", "-o", tempOutputFile.toString(), tempInputFile.toString())); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); + List command = + new ArrayList<>( + Arrays.asList( + "unoconv", + "-vvv", + "-f", + "pdf", + "-o", + tempOutputFile.toString(), + tempInputFile.toString())); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); // Read the converted PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -55,6 +69,7 @@ public class ConvertOfficeController { return pdfBytes; } + private boolean isValidFileExtension(String fileExtension) { String extensionPattern = "^(?i)[a-z0-9]{2,4}$"; return fileExtension.matches(extensionPattern); @@ -62,17 +77,19 @@ public class ConvertOfficeController { @PostMapping(consumes = "multipart/form-data", value = "/file/pdf") @Operation( - summary = "Convert a file to a PDF using LibreOffice", - description = "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO" - ) - public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); + summary = "Convert a file to a PDF using LibreOffice", + description = + "This endpoint converts a given file to a PDF using LibreOffice API Input:Any Output:PDF Type:SISO") + public ResponseEntity processFileToPDF(@ModelAttribute GeneralFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); // unused but can start server instance if startup time is to long // LibreOfficeListener.getInstance().start(); byte[] pdfByteArray = convertToPdf(inputFile); - return WebResponseUtils.bytesToWebResponse(pdfByteArray, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_convertedToPDF.pdf"); + return WebResponseUtils.bytesToWebResponse( + pdfByteArray, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_convertedToPDF.pdf"); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java index 11279a27..74b292b5 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToOffice.java @@ -11,6 +11,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest; import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest; @@ -22,51 +23,70 @@ import stirling.software.SPDF.utils.PDFToFile; @Tag(name = "Convert", description = "Convert APIs") public class ConvertPDFToOffice { - @PostMapping(consumes = "multipart/form-data", value = "/pdf/html") - @Operation(summary = "Convert PDF to HTML", description = "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") - public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/html") + @Operation( + summary = "Convert PDF to HTML", + description = + "This endpoint converts a PDF file to HTML format. Input:PDF Output:HTML Type:SISO") + public ResponseEntity processPdfToHTML(@ModelAttribute PDFFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "html", "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") - @Operation(summary = "Convert PDF to Presentation format", description = "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") - public ResponseEntity processPdfToPresentation(@ModelAttribute PdfToPresentationRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation") + @Operation( + summary = "Convert PDF to Presentation format", + description = + "This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO") + public ResponseEntity processPdfToPresentation( + @ModelAttribute PdfToPresentationRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/text") - @Operation(summary = "Convert PDF to Text or RTF format", description = "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") - public ResponseEntity processPdfToRTForTXT(@ModelAttribute PdfToTextOrRTFRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); + @PostMapping(consumes = "multipart/form-data", value = "/pdf/text") + @Operation( + summary = "Convert PDF to Text or RTF format", + description = + "This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO") + public ResponseEntity processPdfToRTForTXT( + @ModelAttribute PdfToTextOrRTFRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/word") - @Operation(summary = "Convert PDF to Word document", description = "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") - public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String outputFormat = request.getOutputFormat(); - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/word") + @Operation( + summary = "Convert PDF to Word document", + description = + "This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO") + public ResponseEntity processPdfToWord(@ModelAttribute PdfToWordRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String outputFormat = request.getOutputFormat(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import"); + } - @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") - @Operation(summary = "Convert PDF to XML", description = "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") - public ResponseEntity processPdfToXML(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); - - PDFToFile pdfToFile = new PDFToFile(); - return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); - } + @PostMapping(consumes = "multipart/form-data", value = "/pdf/xml") + @Operation( + summary = "Convert PDF to XML", + description = + "This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO") + public ResponseEntity processPdfToXML(@ModelAttribute PDFFile request) + throws Exception { + MultipartFile inputFile = request.getFileInput(); + PDFToFile pdfToFile = new PDFToFile(); + return pdfToFile.processPdfToOfficeFormat(inputFile, "xml", "writer_pdf_import"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java index 32ccb84a..ac8ce031 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertPDFToPDFA.java @@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -24,14 +25,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Convert", description = "Convert APIs") public class ConvertPDFToPDFA { - @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") - @Operation( - summary = "Convert a PDF to a PDF/A", - description = "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity pdfToPdfA(@ModelAttribute PDFFile request) - throws Exception { - MultipartFile inputFile = request.getFileInput(); + @PostMapping(consumes = "multipart/form-data", value = "/pdf/pdfa") + @Operation( + summary = "Convert a PDF to a PDF/A", + description = + "This endpoint converts a PDF file to a PDF/A file. PDF/A is a format designed for long-term archiving of digital documents. Input:PDF Output:PDF Type:SISO") + public ResponseEntity pdfToPdfA(@ModelAttribute PDFFile request) throws Exception { + MultipartFile inputFile = request.getFileInput(); // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); @@ -50,7 +50,9 @@ public class ConvertPDFToPDFA { command.add(tempInputFile.toString()); command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); // Read the optimized PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -60,8 +62,8 @@ public class ConvertPDFToPDFA { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_PDFA.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java index 7c81edf2..bf631c87 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ConvertWebsiteToPDF.java @@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.converters.UrlToPdfRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -25,52 +26,52 @@ import stirling.software.SPDF.utils.WebResponseUtils; @RequestMapping("/api/v1/convert") public class ConvertWebsiteToPDF { - @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") - @Operation( - summary = "Convert a URL to a PDF", - description = "This endpoint fetches content from a URL and converts it to a PDF format." - ) - public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) throws IOException, InterruptedException { - String URL = request.getUrlInput(); + @PostMapping(consumes = "multipart/form-data", value = "/url/pdf") + @Operation( + summary = "Convert a URL to a PDF", + description = + "This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO") + public ResponseEntity urlToPdf(@ModelAttribute UrlToPdfRequest request) + throws IOException, InterruptedException { + String URL = request.getUrlInput(); - // Validate the URL format - if(!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { - throw new IllegalArgumentException("Invalid URL format provided."); - } - Path tempOutputFile = null; - byte[] pdfBytes; - try { - // Prepare the output file path - tempOutputFile = Files.createTempFile("output_", ".pdf"); - - // Prepare the OCRmyPDF command - List command = new ArrayList<>(); - command.add("weasyprint"); - command.add(URL); - command.add(tempOutputFile.toString()); - - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT).runCommandWithOutputHandling(command); - - // Read the optimized PDF file - pdfBytes = Files.readAllBytes(tempOutputFile); - } - finally { - // Clean up the temporary files - Files.delete(tempOutputFile); - } - // Convert URL to a safe filename - String outputFilename = convertURLToFileName(URL); - - return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); - } + // Validate the URL format + if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) { + throw new IllegalArgumentException("Invalid URL format provided."); + } + Path tempOutputFile = null; + byte[] pdfBytes; + try { + // Prepare the output file path + tempOutputFile = Files.createTempFile("output_", ".pdf"); - private String convertURLToFileName(String url) { - String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); - if(safeName.length() > 50) { - safeName = safeName.substring(0, 50); // restrict to 50 characters - } - return safeName + ".pdf"; - } + // Prepare the OCRmyPDF command + List command = new ArrayList<>(); + command.add("weasyprint"); + command.add(URL); + command.add(tempOutputFile.toString()); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); + // Read the optimized PDF file + pdfBytes = Files.readAllBytes(tempOutputFile); + } finally { + // Clean up the temporary files + Files.delete(tempOutputFile); + } + // Convert URL to a safe filename + String outputFilename = convertURLToFileName(URL); + + return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); + } + + private String convertURLToFileName(String url) { + String safeName = url.replaceAll("[^a-zA-Z0-9]", "_"); + if (safeName.length() > 50) { + safeName = safeName.substring(0, 50); // restrict to 50 characters + } + return safeName + ".pdf"; + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java index f6fb69c9..c5b9ea8d 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/converters/ExtractController.java @@ -22,33 +22,37 @@ import com.opencsv.CSVWriter; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.controller.api.CropController; import stirling.software.SPDF.controller.api.strippers.PDFTableStripper; import stirling.software.SPDF.model.api.extract.PDFFilePage; @RestController @RequestMapping("/api/v1/convert") -@Tag(name = "General", description = "General APIs") +@Tag(name = "Convert", description = "Convert APIs") public class ExtractController { private static final Logger logger = LoggerFactory.getLogger(CropController.class); - @PostMapping(value = "/pdf-to-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") - public ResponseEntity PdfToCsv(@ModelAttribute PDFFilePage form) - throws Exception { + @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") + public ResponseEntity PdfToCsv(@ModelAttribute PDFFilePage form) throws Exception { ArrayList tableData = new ArrayList<>(); int columnsCount = 0; - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { + try (PDDocument document = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { final double res = 72; // PDF units are at 72 DPI PDFTableStripper stripper = new PDFTableStripper(); PDPage pdPage = document.getPage(form.getPageId() - 1); stripper.extractTable(pdPage); columnsCount = stripper.getColumns(); for (int c = 0; c < columnsCount; ++c) { - for(int r=0; r notEmptyColumns = new ArrayList<>(); - for (String item: tableData) { - if(!item.trim().isEmpty()){ + for (String item : tableData) { + if (!item.trim().isEmpty()) { notEmptyColumns.add(item); - }else{ + } else { columnsCount--; } } - List fullTable = notEmptyColumns.stream().map((entity)-> - entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList(); + List fullTable = + notEmptyColumns.stream() + .map( + (entity) -> + entity.replace('\n', ' ') + .replace('\r', ' ') + .trim() + .replaceAll("\\s{2,}", "|")) + .toList(); int rowsCount = fullTable.get(0).split("\\|").length; - ArrayList headersList = getTableHeaders(columnsCount,fullTable); - ArrayList recordList = getRecordsList(rowsCount,fullTable); - - if(headersList.size() == 0 && recordList.size() == 0) { - throw new Exception("No table detected, no headers or records found"); + ArrayList headersList = getTableHeaders(columnsCount, fullTable); + ArrayList recordList = getRecordsList(rowsCount, fullTable); + + if (headersList.size() == 0 && recordList.size() == 0) { + throw new Exception("No table detected, no headers or records found"); } - + StringWriter writer = new StringWriter(); try (CSVWriter csvWriter = new CSVWriter(writer)) { csvWriter.writeNext(headersList.toArray(new String[0])); @@ -85,35 +96,41 @@ public class ExtractController { } HttpHeaders headers = new HttpHeaders(); - headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build()); + headers.setContentDisposition( + ContentDisposition.builder("attachment") + .filename( + form.getFileInput() + .getOriginalFilename() + .replaceFirst("[.][^.]+$", "") + + "_extracted.csv") + .build()); headers.setContentType(MediaType.parseMediaType("text/csv")); - return ResponseEntity.ok() - .headers(headers) - .body(writer.toString()); + return ResponseEntity.ok().headers(headers).body(writer.toString()); } - private ArrayList getRecordsList( int rowsCounts ,List items){ + private ArrayList getRecordsList(int rowsCounts, List items) { ArrayList recordsList = new ArrayList<>(); - for (int b=1; b getTableHeaders(int columnsCount, List items){ + + private ArrayList getTableHeaders(int columnsCount, List items) { ArrayList resultList = new ArrayList<>(); - for (int i=0;i containsText(@ModelAttribute ContainsTextRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String text = request.getText(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasText(pdfDocument, pageNumber, text)) - return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); - return null; - } + @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text") + @Operation( + summary = "Checks if a PDF contains set text, returns true if does", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity containsText(@ModelAttribute ContainsTextRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String text = request.getText(); + String pageNumber = request.getPageNumbers(); - // TODO - @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") - @Operation(summary = "Checks if a PDF contains an image", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) - throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageNumber = request.getPageNumbers(); - - PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); - if (PdfUtils.hasImages(pdfDocument, pageNumber)) - return WebResponseUtils.pdfDocToWebResponse(pdfDocument, inputFile.getOriginalFilename()); - return null; - } + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasText(pdfDocument, pageNumber, text)) + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, inputFile.getOriginalFilename()); + return null; + } - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") - @Operation(summary = "Checks if a PDF is greater, less or equal to a setPageCount", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageCount(@ModelAttribute PDFComparisonAndCount request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String pageCount = request.getPageCount(); - String comparator = request.getComparator(); - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); - int actualPageCount = document.getNumberOfPages(); + // TODO + @PostMapping(consumes = "multipart/form-data", value = "/filter-contains-image") + @Operation( + summary = "Checks if a PDF contains an image", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity containsImage(@ModelAttribute PDFWithPageNums request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageNumber = request.getPageNumbers(); - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualPageCount > Integer.parseInt(pageCount); - break; - case "Equal": - valid = actualPageCount == Integer.parseInt(pageCount); - break; - case "Less": - valid = actualPageCount < Integer.parseInt(pageCount); - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } + PDDocument pdfDocument = PDDocument.load(inputFile.getInputStream()); + if (PdfUtils.hasImages(pdfDocument, pageNumber)) + return WebResponseUtils.pdfDocToWebResponse( + pdfDocument, inputFile.getOriginalFilename()); + return null; + } - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-count") + @Operation( + summary = "Checks if a PDF is greater, less or equal to a setPageCount", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageCount(@ModelAttribute PDFComparisonAndCount request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String pageCount = request.getPageCount(); + String comparator = request.getComparator(); + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); + int actualPageCount = document.getNumberOfPages(); - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") - @Operation(summary = "Checks if a PDF is of a certain size", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageSize(@ModelAttribute PageSizeRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String standardPageSize = request.getStandardPageSize(); - String comparator = request.getComparator(); + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualPageCount > Integer.parseInt(pageCount); + break; + case "Equal": + valid = actualPageCount == Integer.parseInt(pageCount); + break; + case "Less": + valid = actualPageCount < Integer.parseInt(pageCount); + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - PDPage firstPage = document.getPage(0); - PDRectangle actualPageSize = firstPage.getMediaBox(); + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-size") + @Operation( + summary = "Checks if a PDF is of a certain size", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageSize(@ModelAttribute PageSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String standardPageSize = request.getStandardPageSize(); + String comparator = request.getComparator(); - // Calculate the area of the actual page size - float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); - // Get the standard size and calculate its area - PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); - float standardArea = standardSize.getWidth() * standardSize.getHeight(); + PDPage firstPage = document.getPage(0); + PDRectangle actualPageSize = firstPage.getMediaBox(); - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualArea > standardArea; - break; - case "Equal": - valid = actualArea == standardArea; - break; - case "Less": - valid = actualArea < standardArea; - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } + // Calculate the area of the actual page size + float actualArea = actualPageSize.getWidth() * actualPageSize.getHeight(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } + // Get the standard size and calculate its area + PDRectangle standardSize = PdfUtils.textToPageSize(standardPageSize); + float standardArea = standardSize.getWidth() * standardSize.getHeight(); - @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") - @Operation(summary = "Checks if a PDF is a set file size", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity fileSize(@ModelAttribute FileSizeRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - String fileSize = request.getFileSize(); - String comparator = request.getComparator(); + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualArea > standardArea; + break; + case "Equal": + valid = actualArea == standardArea; + break; + case "Less": + valid = actualArea < standardArea; + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } - // Get the file size - long actualFileSize = inputFile.getSize(); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualFileSize > Long.parseLong(fileSize); - break; - case "Equal": - valid = actualFileSize == Long.parseLong(fileSize); - break; - case "Less": - valid = actualFileSize < Long.parseLong(fileSize); - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } + @PostMapping(consumes = "multipart/form-data", value = "/filter-file-size") + @Operation( + summary = "Checks if a PDF is a set file size", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity fileSize(@ModelAttribute FileSizeRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + String fileSize = request.getFileSize(); + String comparator = request.getComparator(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; - } + // Get the file size + long actualFileSize = inputFile.getSize(); - @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") - @Operation(summary = "Checks if a PDF is of a certain rotation", description = "Input:PDF Output:Boolean Type:SISO") - public ResponseEntity pageRotation(@ModelAttribute PageRotationRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - int rotation = request.getRotation(); - String comparator = request.getComparator(); + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualFileSize > Long.parseLong(fileSize); + break; + case "Equal": + valid = actualFileSize == Long.parseLong(fileSize); + break; + case "Less": + valid = actualFileSize < Long.parseLong(fileSize); + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } - // Load the PDF - PDDocument document = PDDocument.load(inputFile.getInputStream()); + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } - // Get the rotation of the first page - PDPage firstPage = document.getPage(0); - int actualRotation = firstPage.getRotation(); - boolean valid = false; - // Perform the comparison - switch (comparator) { - case "Greater": - valid = actualRotation > rotation; - break; - case "Equal": - valid = actualRotation == rotation; - break; - case "Less": - valid = actualRotation < rotation; - break; - default: - throw new IllegalArgumentException("Invalid comparator: " + comparator); - } + @PostMapping(consumes = "multipart/form-data", value = "/filter-page-rotation") + @Operation( + summary = "Checks if a PDF is of a certain rotation", + description = "Input:PDF Output:Boolean Type:SISO") + public ResponseEntity pageRotation(@ModelAttribute PageRotationRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + int rotation = request.getRotation(); + String comparator = request.getComparator(); - if (valid) - return WebResponseUtils.multiPartFileToWebResponse(inputFile); - return null; + // Load the PDF + PDDocument document = PDDocument.load(inputFile.getInputStream()); - } + // Get the rotation of the first page + PDPage firstPage = document.getPage(0); + int actualRotation = firstPage.getRotation(); + boolean valid = false; + // Perform the comparison + switch (comparator) { + case "Greater": + valid = actualRotation > rotation; + break; + case "Equal": + valid = actualRotation == rotation; + break; + case "Less": + valid = actualRotation < rotation; + break; + default: + throw new IllegalArgumentException("Invalid comparator: " + comparator); + } + if (valid) return WebResponseUtils.multiPartFileToWebResponse(inputFile); + return null; + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java index fe8337d2..e81ef1e1 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoRenameController.java @@ -19,8 +19,10 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -32,97 +34,105 @@ public class AutoRenameController { private static final int LINE_LIMIT = 11; @PostMapping(consumes = "multipart/form-data", value = "/auto-rename") - @Operation(summary = "Extract header from PDF file", description = "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") - public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) throws Exception { + @Operation( + summary = "Extract header from PDF file", + description = + "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO") + public ResponseEntity extractHeader(@ModelAttribute ExtractHeaderRequest request) + throws Exception { MultipartFile file = request.getFileInput(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); - PDDocument document = PDDocument.load(file.getInputStream()); - PDFTextStripper reader = new PDFTextStripper() { - class LineInfo { - String text; - float fontSize; + PDDocument document = PDDocument.load(file.getInputStream()); + PDFTextStripper reader = + new PDFTextStripper() { + class LineInfo { + String text; + float fontSize; - LineInfo(String text, float fontSize) { - this.text = text; - this.fontSize = fontSize; - } - } + LineInfo(String text, float fontSize) { + this.text = text; + this.fontSize = fontSize; + } + } - List lineInfos = new ArrayList<>(); - StringBuilder lineBuilder = new StringBuilder(); - float lastY = -1; - float maxFontSizeInLine = 0.0f; - int lineCount = 0; + List lineInfos = new ArrayList<>(); + StringBuilder lineBuilder = new StringBuilder(); + float lastY = -1; + float maxFontSizeInLine = 0.0f; + int lineCount = 0; - @Override - protected void processTextPosition(TextPosition text) { - if (lastY != text.getY() && lineCount < LINE_LIMIT) { - processLine(); - lineBuilder = new StringBuilder(text.getUnicode()); - maxFontSizeInLine = text.getFontSizeInPt(); - lastY = text.getY(); - lineCount++; - } else if (lineCount < LINE_LIMIT) { - lineBuilder.append(text.getUnicode()); - if (text.getFontSizeInPt() > maxFontSizeInLine) { - maxFontSizeInLine = text.getFontSizeInPt(); - } - } - } + @Override + protected void processTextPosition(TextPosition text) { + if (lastY != text.getY() && lineCount < LINE_LIMIT) { + processLine(); + lineBuilder = new StringBuilder(text.getUnicode()); + maxFontSizeInLine = text.getFontSizeInPt(); + lastY = text.getY(); + lineCount++; + } else if (lineCount < LINE_LIMIT) { + lineBuilder.append(text.getUnicode()); + if (text.getFontSizeInPt() > maxFontSizeInLine) { + maxFontSizeInLine = text.getFontSizeInPt(); + } + } + } - private void processLine() { - if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) { - lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine)); - } - } + private void processLine() { + if (lineBuilder.length() > 0 && lineCount < LINE_LIMIT) { + lineInfos.add(new LineInfo(lineBuilder.toString(), maxFontSizeInLine)); + } + } - @Override - public String getText(PDDocument doc) throws IOException { - this.lineInfos.clear(); - this.lineBuilder = new StringBuilder(); - this.lastY = -1; - this.maxFontSizeInLine = 0.0f; - this.lineCount = 0; - super.getText(doc); - processLine(); // Process the last line + @Override + public String getText(PDDocument doc) throws IOException { + this.lineInfos.clear(); + this.lineBuilder = new StringBuilder(); + this.lastY = -1; + this.maxFontSizeInLine = 0.0f; + this.lineCount = 0; + super.getText(doc); + processLine(); // Process the last line - // Merge lines with same font size - List mergedLineInfos = new ArrayList<>(); - for (int i = 0; i < lineInfos.size(); i++) { - String mergedText = lineInfos.get(i).text; - float fontSize = lineInfos.get(i).fontSize; - while (i + 1 < lineInfos.size() && lineInfos.get(i + 1).fontSize == fontSize) { - mergedText += " " + lineInfos.get(i + 1).text; - i++; - } - mergedLineInfos.add(new LineInfo(mergedText, fontSize)); - } + // Merge lines with same font size + List mergedLineInfos = new ArrayList<>(); + for (int i = 0; i < lineInfos.size(); i++) { + String mergedText = lineInfos.get(i).text; + float fontSize = lineInfos.get(i).fontSize; + while (i + 1 < lineInfos.size() + && lineInfos.get(i + 1).fontSize == fontSize) { + mergedText += " " + lineInfos.get(i + 1).text; + i++; + } + mergedLineInfos.add(new LineInfo(mergedText, fontSize)); + } - // Sort lines by font size in descending order and get the first one - mergedLineInfos.sort(Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); - String title = mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; + // Sort lines by font size in descending order and get the first one + mergedLineInfos.sort( + Comparator.comparing((LineInfo li) -> li.fontSize).reversed()); + String title = + mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(0).text; - return title != null ? title : (useFirstTextAsFallback ? (mergedLineInfos.isEmpty() ? null : mergedLineInfos.get(mergedLineInfos.size() - 1).text) : null); - } + return title != null + ? title + : (useFirstTextAsFallback + ? (mergedLineInfos.isEmpty() + ? null + : mergedLineInfos.get(mergedLineInfos.size() - 1) + .text) + : null); + } + }; - }; + String header = reader.getText(document); - String header = reader.getText(document); - - - // Sanitize the header string by removing characters not allowed in a filename. if (header != null && header.length() < 255) { header = header.replaceAll("[/\\\\?%*:|\"<>]", ""); return WebResponseUtils.pdfDocToWebResponse(document, header + ".pdf"); } else { - logger.info("File has no good title to be found"); - return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename()); + logger.info("File has no good title to be found"); + return WebResponseUtils.pdfDocToWebResponse(document, file.getOriginalFilename()); } } - - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java index b4b9b951..e32e325e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/AutoSplitPdfController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.misc; + import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; import java.awt.image.DataBufferInt; @@ -32,6 +33,7 @@ import com.google.zxing.common.HybridBinarizer; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -43,8 +45,12 @@ public class AutoSplitPdfController { private static final String QR_CONTENT = "https://github.com/Frooodle/Stirling-PDF"; @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") - public ResponseEntity autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) throws IOException { + @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 autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) + throws IOException { MultipartFile file = request.getFileInput(); boolean duplexMode = request.isDuplexMode(); @@ -107,29 +113,48 @@ public class AutoSplitPdfController { } catch (Exception e) { e.printStackTrace(); } finally { - data = Files.readAllBytes(zipFile); + data = Files.readAllBytes(zipFile); Files.delete(zipFile); } - return WebResponseUtils.bytesToWebResponse(data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); } - private static String decodeQRCode(BufferedImage bufferedImage) { LuminanceSource source; if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferByte) { byte[] pixels = ((DataBufferByte) bufferedImage.getRaster().getDataBuffer()).getData(); - source = new PlanarYUVLuminanceSource(pixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + source = + new PlanarYUVLuminanceSource( + pixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); } else if (bufferedImage.getRaster().getDataBuffer() instanceof DataBufferInt) { int[] pixels = ((DataBufferInt) bufferedImage.getRaster().getDataBuffer()).getData(); byte[] newPixels = new byte[pixels.length]; for (int i = 0; i < pixels.length; i++) { newPixels[i] = (byte) (pixels[i] & 0xff); } - source = new PlanarYUVLuminanceSource(newPixels, bufferedImage.getWidth(), bufferedImage.getHeight(), 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight(), false); + source = + new PlanarYUVLuminanceSource( + newPixels, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + 0, + 0, + bufferedImage.getWidth(), + bufferedImage.getHeight(), + false); } else { - throw new IllegalArgumentException("BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); + throw new IllegalArgumentException( + "BufferedImage must have 8-bit gray scale, 24-bit RGB, 32-bit ARGB (packed int), byte gray, or 3-byte/4-byte RGB image data"); } BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java index c52ff61a..036d6a66 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/BlankPageController.java @@ -28,6 +28,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest; import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -39,17 +40,18 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class BlankPageController { - @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") - @Operation( - summary = "Remove blank pages from a PDF file", - description = "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); - int threshold = request.getThreshold(); - float whitePercent = request.getWhitePercent(); - - PDDocument document = null; + @PostMapping(consumes = "multipart/form-data", value = "/remove-blanks") + @Operation( + summary = "Remove blank pages from a PDF file", + description = + "This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO") + public ResponseEntity removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); + int threshold = request.getThreshold(); + float whitePercent = request.getWhitePercent(); + + PDDocument document = null; try { document = PDDocument.load(inputFile.getInputStream()); PDPageTree pages = document.getDocumentCatalog().getPages(); @@ -72,21 +74,34 @@ public class BlankPageController { boolean hasImages = PdfUtils.hasImagesOnPage(page); if (hasImages) { System.out.println("page " + pageIndex + " has image"); - + Path tempFile = Files.createTempFile("image_", ".png"); - + // Render image and save as temp file BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 300); ImageIO.write(image, "png", tempFile.toFile()); - - List command = new ArrayList<>(Arrays.asList("python3", System.getProperty("user.dir") + "/scripts/detect-blank-pages.py", tempFile.toString() ,"--threshold", String.valueOf(threshold), "--white_percent", String.valueOf(whitePercent))); - + + List command = + new ArrayList<>( + Arrays.asList( + "python3", + System.getProperty("user.dir") + + "/scripts/detect-blank-pages.py", + tempFile.toString(), + "--threshold", + String.valueOf(threshold), + "--white_percent", + String.valueOf(whitePercent))); + // Run CLI command - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); - + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) + .runCommandWithOutputHandling(command); + // does contain data if (returnCode.getRc() == 0) { - System.out.println("page " + pageIndex + " has image which is not blank"); + System.out.println( + "page " + pageIndex + " has image which is not blank"); pagesToKeepIndex.add(pageIndex); } else { System.out.println("Skipping, Image was blank for page #" + pageIndex); @@ -94,12 +109,12 @@ public class BlankPageController { } } pageIndex++; - } System.out.print("pagesToKeep=" + pagesToKeepIndex.size()); // Remove pages not present in pagesToKeepIndex - List pageIndices = IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); + List pageIndices = + IntStream.range(0, pages.getCount()).boxed().collect(Collectors.toList()); Collections.reverse(pageIndices); // Reverse to prevent index shifting during removal for (Integer i : pageIndices) { if (!pagesToKeepIndex.contains(i)) { @@ -107,16 +122,15 @@ public class BlankPageController { } } - return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_blanksRemoved.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_blanksRemoved.pdf"); } catch (IOException e) { e.printStackTrace(); return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); } finally { - if (document != null) - document.close(); + if (document != null) document.close(); } } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java index dd864bc1..fd9a0460 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/CompressController.java @@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.ProcessExecutor; @@ -44,20 +45,23 @@ public class CompressController { private static final Logger logger = LoggerFactory.getLogger(CompressController.class); @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf") - @Operation(summary = "Optimize PDF file", description = "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") - public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) throws Exception { + @Operation( + summary = "Optimize PDF file", + description = + "This endpoint accepts a PDF file and optimizes it based on the provided parameters. Input:PDF Output:PDF Type:SISO") + public ResponseEntity optimizePdf(@ModelAttribute OptimizePdfRequest request) + throws Exception { MultipartFile inputFile = request.getFileInput(); Integer optimizeLevel = request.getOptimizeLevel(); String expectedOutputSizeString = request.getExpectedOutputSize(); - - if(expectedOutputSizeString == null && optimizeLevel == null) { + if (expectedOutputSizeString == null && optimizeLevel == null) { throw new Exception("Both expected output size and optimize level are not specified"); } Long expectedOutputSize = 0L; boolean autoMode = false; - if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1 ) { + if (expectedOutputSizeString != null && expectedOutputSizeString.length() > 1) { expectedOutputSize = GeneralUtils.convertSizeToBytes(expectedOutputSizeString); autoMode = true; } @@ -71,8 +75,9 @@ public class CompressController { // Prepare the output file path Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - // Determine initial optimization level based on expected size reduction, only if in autoMode - if(autoMode) { + // Determine initial optimization level based on expected size reduction, only if in + // autoMode + if (autoMode) { double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; if (sizeReductionRatio > 0.7) { optimizeLevel = 1; @@ -94,20 +99,20 @@ public class CompressController { command.add("-dCompatibilityLevel=1.4"); switch (optimizeLevel) { - case 1: - command.add("-dPDFSETTINGS=/prepress"); - break; - case 2: - command.add("-dPDFSETTINGS=/printer"); - break; - case 3: - command.add("-dPDFSETTINGS=/ebook"); - break; - case 4: - command.add("-dPDFSETTINGS=/screen"); - break; - default: - command.add("-dPDFSETTINGS=/default"); + case 1: + command.add("-dPDFSETTINGS=/prepress"); + break; + case 2: + command.add("-dPDFSETTINGS=/printer"); + break; + case 3: + command.add("-dPDFSETTINGS=/ebook"); + break; + case 4: + command.add("-dPDFSETTINGS=/screen"); + break; + default: + command.add("-dPDFSETTINGS=/default"); } command.add("-dNOPAUSE"); @@ -116,7 +121,9 @@ public class CompressController { command.add("-sOutputFile=" + tempOutputFile.toString()); command.add(tempInputFile.toString()); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); // Check if file size is within expected size or not auto mode so instantly finish long outputFileSize = Files.size(tempOutputFile); @@ -125,19 +132,18 @@ public class CompressController { } else { // Increase optimization level for next iteration optimizeLevel++; - if(autoMode && optimizeLevel > 3) { + if (autoMode && optimizeLevel > 3) { System.out.println("Skipping level 4 due to bad results in auto mode"); sizeMet = true; - } else if(optimizeLevel == 5) { - + } else if (optimizeLevel == 5) { + } else { - System.out.println("Increasing ghostscript optimisation level to " + optimizeLevel); + System.out.println( + "Increasing ghostscript optimisation level to " + optimizeLevel); } } } - - if (expectedOutputSize != null && autoMode) { long outputFileSize = Files.size(tempOutputFile); if (outputFileSize > expectedOutputSize) { @@ -157,8 +163,8 @@ public class CompressController { BufferedImage bufferedImage = image.getImage(); // Calculate the new dimensions - int newWidth = (int)(bufferedImage.getWidth() * scaleFactor); - int newHeight = (int)(bufferedImage.getHeight() * scaleFactor); + int newWidth = (int) (bufferedImage.getWidth() * scaleFactor); + int newHeight = (int) (bufferedImage.getHeight() * scaleFactor); // If the new dimensions are zero, skip this iteration if (newWidth == 0 || newHeight == 0) { @@ -166,23 +172,39 @@ public class CompressController { } // Otherwise, proceed with the scaling - Image scaledImage = bufferedImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH); + Image scaledImage = + bufferedImage.getScaledInstance( + newWidth, newHeight, Image.SCALE_SMOOTH); // Convert the scaled image back to a BufferedImage - BufferedImage scaledBufferedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); - scaledBufferedImage.getGraphics().drawImage(scaledImage, 0, 0, null); + BufferedImage scaledBufferedImage = + new BufferedImage( + newWidth, + newHeight, + BufferedImage.TYPE_INT_RGB); + scaledBufferedImage + .getGraphics() + .drawImage(scaledImage, 0, 0, null); // Compress the scaled image - ByteArrayOutputStream compressedImageStream = new ByteArrayOutputStream(); - ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream); + ByteArrayOutputStream compressedImageStream = + new ByteArrayOutputStream(); + ImageIO.write( + scaledBufferedImage, "jpeg", compressedImageStream); byte[] imageBytes = compressedImageStream.toByteArray(); compressedImageStream.close(); // Convert compressed image back to PDImageXObject - ByteArrayInputStream bais = new ByteArrayInputStream(imageBytes); - PDImageXObject compressedImage = PDImageXObject.createFromByteArray(doc, imageBytes, image.getCOSObject().toString()); + ByteArrayInputStream bais = + new ByteArrayInputStream(imageBytes); + PDImageXObject compressedImage = + PDImageXObject.createFromByteArray( + doc, + imageBytes, + image.getCOSObject().toString()); - // Replace the image in the resources with the compressed version + // Replace the image in the resources with the compressed + // version res.put(name, compressedImage); } } @@ -194,16 +216,23 @@ public class CompressController { long currentSize = Files.size(tempOutputFile); // Check if the overall PDF size is still larger than expectedOutputSize if (currentSize > expectedOutputSize) { - // Log the current file size and scaleFactor - - System.out.println("Current file size: " + FileUtils.byteCountToDisplaySize(currentSize)); + // Log the current file size and scaleFactor + + System.out.println( + "Current file size: " + + FileUtils.byteCountToDisplaySize(currentSize)); System.out.println("Current scale factor: " + scaleFactor); // The file is still too large, reduce scaleFactor and try again scaleFactor *= 0.9; // reduce scaleFactor by 10% // Avoid scaleFactor being too small, causing the image to shrink to 0 - if(scaleFactor < 0.2 || previousFileSize == currentSize){ - throw new RuntimeException("Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + FileUtils.byteCountToDisplaySize(currentSize) + ", " + currentSize + " bytes"); + if (scaleFactor < 0.2 || previousFileSize == currentSize) { + throw new RuntimeException( + "Could not reach the desired size without excessively degrading image quality, lowest size recommended is " + + FileUtils.byteCountToDisplaySize(currentSize) + + ", " + + currentSize + + " bytes"); } previousFileSize = currentSize; } else { @@ -211,10 +240,7 @@ public class CompressController { break; } } - } - - } } @@ -222,9 +248,10 @@ public class CompressController { byte[] pdfBytes = Files.readAllBytes(tempOutputFile); // Check if optimized file is larger than the original - if(pdfBytes.length > inputFileSize) { + if (pdfBytes.length > inputFileSize) { // Log the occurrence - logger.warn("Optimized file is larger than the original. Returning the original file instead."); + logger.warn( + "Optimized file is larger than the original. Returning the original file instead."); // Read the original file again pdfBytes = Files.readAllBytes(tempInputFile); @@ -235,8 +262,8 @@ public class CompressController { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java index d5906970..257f4d52 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImageScansController.java @@ -32,10 +32,12 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -44,18 +46,28 @@ public class ExtractImageScansController { private static final Logger logger = LoggerFactory.getLogger(ExtractImageScansController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans") - @Operation(summary = "Extract image scans from an input file", - description = "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") + @Operation( + summary = "Extract image scans from an input file", + description = + "This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO") public ResponseEntity extractImageScans( - @RequestBody( - description = "Form data containing file and extraction parameters", - required = true, - content = @Content( - mediaType = "multipart/form-data", - schema = @Schema(implementation = ExtractImageScansRequest.class) // This should represent your form's structure - ) - ) - ExtractImageScansRequest form) throws IOException, InterruptedException { + @RequestBody( + description = "Form data containing file and extraction parameters", + required = true, + content = + @Content( + mediaType = "multipart/form-data", + schema = + @Schema( + implementation = + ExtractImageScansRequest + .class) // This should + // represent + // your form's + // structure + )) + ExtractImageScansRequest form) + throws IOException, InterruptedException { String fileName = form.getFileInput().getOriginalFilename(); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -64,7 +76,8 @@ public class ExtractImageScansController { // Check if input file is a PDF if (extension.equalsIgnoreCase("pdf")) { // Load PDF document - try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { + try (PDDocument document = + PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { PDFRenderer pdfRenderer = new PDFRenderer(document); int pageCount = document.getNumberOfPages(); images = new ArrayList<>(); @@ -84,7 +97,10 @@ public class ExtractImageScansController { } } else { Path tempInputFile = Files.createTempFile("input_", "." + extension); - Files.copy(form.getFileInput().getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + form.getFileInput().getInputStream(), + tempInputFile, + StandardCopyOption.REPLACE_EXISTING); // Add input file path to images list images.add(tempInputFile.toString()); } @@ -95,21 +111,28 @@ public class ExtractImageScansController { for (int i = 0; i < images.size(); i++) { Path tempDir = Files.createTempDirectory("openCV_output"); - List command = new ArrayList<>(Arrays.asList( - "python3", - "./scripts/split_photos.py", - images.get(i), - tempDir.toString(), - "--angle_threshold", String.valueOf(form.getAngleThreshold()), - "--tolerance", String.valueOf(form.getTolerance()), - "--min_area", String.valueOf(form.getMinArea()), - "--min_contour_area", String.valueOf(form.getMinContourArea()), - "--border_size", String.valueOf(form.getBorderSize()) - )); - + List command = + new ArrayList<>( + Arrays.asList( + "python3", + "./scripts/split_photos.py", + images.get(i), + tempDir.toString(), + "--angle_threshold", + String.valueOf(form.getAngleThreshold()), + "--tolerance", + String.valueOf(form.getTolerance()), + "--min_area", + String.valueOf(form.getMinArea()), + "--min_contour_area", + String.valueOf(form.getMinContourArea()), + "--border_size", + String.valueOf(form.getBorderSize()))); // Run CLI command - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.PYTHON_OPENCV) + .runCommandWithOutputHandling(command); // Read the output photos in temp directory List tempOutputFiles = Files.list(tempDir).sorted().collect(Collectors.toList()); @@ -126,10 +149,16 @@ public class ExtractImageScansController { String outputZipFilename = fileName.replaceFirst("[.][^.]+$", "") + "_processed.zip"; Path tempZipFile = Files.createTempFile("output_", ".zip"); - try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + try (ZipOutputStream zipOut = + new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { // Add processed images to the zip for (int i = 0; i < processedImageBytes.size(); i++) { - ZipEntry entry = new ZipEntry(fileName.replaceFirst("[.][^.]+$", "") + "_" + (i + 1) + ".png"); + ZipEntry entry = + new ZipEntry( + fileName.replaceFirst("[.][^.]+$", "") + + "_" + + (i + 1) + + ".png"); zipOut.putNextEntry(entry); zipOut.write(processedImageBytes.get(i)); zipOut.closeEntry(); @@ -141,13 +170,15 @@ public class ExtractImageScansController { // Clean up the temporary zip file Files.delete(tempZipFile); - return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } else { // Return the processed image as a response byte[] imageBytes = processedImageBytes.get(0); - return WebResponseUtils.bytesToWebResponse(imageBytes, fileName.replaceFirst("[.][^.]+$", "") + ".png", MediaType.IMAGE_PNG); + return WebResponseUtils.bytesToWebResponse( + imageBytes, + fileName.replaceFirst("[.][^.]+$", "") + ".png", + MediaType.IMAGE_PNG); } - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java index 6e18f1f2..f436d9f6 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ExtractImagesController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.misc; + import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; @@ -29,8 +30,10 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFWithImageFormatRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") @@ -39,13 +42,17 @@ public class ExtractImagesController { private static final Logger logger = LoggerFactory.getLogger(ExtractImagesController.class); @PostMapping(consumes = "multipart/form-data", value = "/extract-images") - @Operation(summary = "Extract images from a PDF file", - description = "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") - public ResponseEntity extractImages(@ModelAttribute PDFWithImageFormatRequest request) throws IOException { + @Operation( + summary = "Extract images from a PDF file", + description = + "This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO") + public ResponseEntity extractImages(@ModelAttribute PDFWithImageFormatRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String format = request.getFormat(); - System.out.println(System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); + System.out.println( + System.currentTimeMillis() + "file=" + file.getName() + ", format=" + format); PDDocument document = PDDocument.load(file.getBytes()); // Create ByteArrayOutputStream to write zip file to byte array @@ -69,24 +76,37 @@ public class ExtractImagesController { if (page.getResources().isImageXObject(name)) { PDImageXObject image = (PDImageXObject) page.getResources().getXObject(name); int imageHash = image.hashCode(); - if(processedImages.contains(imageHash)) { + if (processedImages.contains(imageHash)) { continue; // Skip already processed images } processedImages.add(imageHash); - + // Convert image to desired format RenderedImage renderedImage = image.getImage(); BufferedImage bufferedImage = null; if (format.equalsIgnoreCase("png")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_ARGB); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_INT_ARGB); } else if (format.equalsIgnoreCase("jpeg") || format.equalsIgnoreCase("jpg")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_INT_RGB); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_INT_RGB); } else if (format.equalsIgnoreCase("gif")) { - bufferedImage = new BufferedImage(renderedImage.getWidth(), renderedImage.getHeight(), BufferedImage.TYPE_BYTE_INDEXED); + bufferedImage = + new BufferedImage( + renderedImage.getWidth(), + renderedImage.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED); } // Write image to zip file - String imageName = filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; + String imageName = + filename + "_" + imageIndex + " (Page " + pageNum + ")." + format; ZipEntry zipEntry = new ZipEntry(imageName); zos.putNextEntry(zipEntry); @@ -111,7 +131,7 @@ public class ExtractImagesController { // Create ByteArrayResource from byte array byte[] zipContents = baos.toByteArray(); - return WebResponseUtils.boasToWebResponse(baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.boasToWebResponse( + baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java index 099e8411..e9885f1e 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/FakeScanControllerWIP.java @@ -3,21 +3,17 @@ package stirling.software.SPDF.controller.api.misc; import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; -//Required for image manipulation import java.awt.image.BufferedImage; import java.awt.image.BufferedImageOp; import java.awt.image.ConvolveOp; import java.awt.image.Kernel; import java.awt.image.RescaleOp; import java.io.ByteArrayOutputStream; -//Required for file input/output import java.io.File; import java.io.IOException; import java.security.SecureRandom; -//Other required classes import java.util.Random; -//Required for image input/output import javax.imageio.ImageIO; import org.apache.pdfbox.pdmodel.PDDocument; @@ -40,6 +36,7 @@ import org.springframework.web.multipart.MultipartFile; 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.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; @@ -50,102 +47,101 @@ public class FakeScanControllerWIP { private static final Logger logger = LoggerFactory.getLogger(FakeScanControllerWIP.class); - //TODO + // TODO @Hidden @PostMapping(consumes = "multipart/form-data", value = "/fakeScan") @Operation( - summary = "Repair a PDF file", - description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response." - ) + summary = "Repair a PDF file", + description = + "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response.") public ResponseEntity repairPdf(@ModelAttribute PDFFile request) throws IOException { MultipartFile inputFile = request.getFileInput(); - PDDocument document = PDDocument.load(inputFile.getBytes()); - PDFRenderer pdfRenderer = new PDFRenderer(document); - for (int page = 0; page < document.getNumberOfPages(); ++page) - { - BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); - ImageIO.write(image, "png", new File("scanned-" + (page+1) + ".png")); - } - document.close(); + PDDocument document = PDDocument.load(inputFile.getBytes()); + PDFRenderer pdfRenderer = new PDFRenderer(document); + for (int page = 0; page < document.getNumberOfPages(); ++page) { + BufferedImage image = pdfRenderer.renderImageWithDPI(page, 300, ImageType.RGB); + ImageIO.write(image, "png", new File("scanned-" + (page + 1) + ".png")); + } + document.close(); - // Constants - int scannedness = 90; // Value between 0 and 100 - int dirtiness = 0; // Value between 0 and 100 + // Constants + int scannedness = 90; // Value between 0 and 100 + int dirtiness = 0; // Value between 0 and 100 - // Load the source image - BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); + // Load the source image + BufferedImage sourceImage = ImageIO.read(new File("scanned-1.png")); - // Create the destination image - BufferedImage destinationImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); + // Create the destination image + BufferedImage destinationImage = + new BufferedImage( + sourceImage.getWidth(), sourceImage.getHeight(), sourceImage.getType()); - // Apply a brightness and contrast effect based on the "scanned-ness" - float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 - float offset = scannedness * 1.5f; // Between 0 and 150 - BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); - op.filter(sourceImage, destinationImage); + // Apply a brightness and contrast effect based on the "scanned-ness" + float scaleFactor = 1.0f + (scannedness / 100.0f) * 0.5f; // Between 1.0 and 1.5 + float offset = scannedness * 1.5f; // Between 0 and 150 + BufferedImageOp op = new RescaleOp(scaleFactor, offset, null); + op.filter(sourceImage, destinationImage); - // Apply a rotation effect - double rotationRequired = Math.toRadians((new SecureRandom().nextInt(3 - 1) + 1)); // Random angle between 1 and 3 degrees - double locationX = destinationImage.getWidth() / 2; - double locationY = destinationImage.getHeight() / 2; - AffineTransform tx = AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); - AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); - destinationImage = rotateOp.filter(destinationImage, null); + // Apply a rotation effect + double rotationRequired = + Math.toRadians( + (new SecureRandom().nextInt(3 - 1) + + 1)); // Random angle between 1 and 3 degrees + double locationX = destinationImage.getWidth() / 2; + double locationY = destinationImage.getHeight() / 2; + AffineTransform tx = + AffineTransform.getRotateInstance(rotationRequired, locationX, locationY); + AffineTransformOp rotateOp = new AffineTransformOp(tx, AffineTransformOp.TYPE_BILINEAR); + destinationImage = rotateOp.filter(destinationImage, null); - // Apply a blur effect based on the "scanned-ness" - float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 - float[] matrix = { - blurIntensity, blurIntensity, blurIntensity, - blurIntensity, blurIntensity, blurIntensity, - blurIntensity, blurIntensity, blurIntensity - }; - BufferedImageOp blurOp = new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); - destinationImage = blurOp.filter(destinationImage, null); + // Apply a blur effect based on the "scanned-ness" + float blurIntensity = scannedness / 100.0f * 0.2f; // Between 0.0 and 0.2 + float[] matrix = { + blurIntensity, blurIntensity, blurIntensity, + blurIntensity, blurIntensity, blurIntensity, + blurIntensity, blurIntensity, blurIntensity + }; + BufferedImageOp blurOp = + new ConvolveOp(new Kernel(3, 3, matrix), ConvolveOp.EDGE_NO_OP, null); + destinationImage = blurOp.filter(destinationImage, null); - // Add noise to the image based on the "dirtiness" - Random random = new SecureRandom(); - for (int y = 0; y < destinationImage.getHeight(); y++) { - for (int x = 0; x < destinationImage.getWidth(); x++) { - if (random.nextInt(100) < dirtiness) { - // Change the pixel color to black randomly based on the "dirtiness" - destinationImage.setRGB(x, y, Color.BLACK.getRGB()); - } - } - } + // Add noise to the image based on the "dirtiness" + Random random = new SecureRandom(); + for (int y = 0; y < destinationImage.getHeight(); y++) { + for (int x = 0; x < destinationImage.getWidth(); x++) { + if (random.nextInt(100) < dirtiness) { + // Change the pixel color to black randomly based on the "dirtiness" + destinationImage.setRGB(x, y, Color.BLACK.getRGB()); + } + } + } - // Save the image - ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); + // Save the image + ImageIO.write(destinationImage, "PNG", new File("scanned-1.png")); + PDDocument documentOut = new PDDocument(); + for (int page = 1; page <= document.getNumberOfPages(); ++page) { + BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); - - - - + // Adjust the dimensions of the page + PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); + documentOut.addPage(pdPage); - PDDocument documentOut = new PDDocument(); - for (int page = 1; page <= document.getNumberOfPages(); ++page) - { - BufferedImage bim = ImageIO.read(new File("scanned-" + page + ".png")); - - // Adjust the dimensions of the page - PDPage pdPage = new PDPage(new PDRectangle(bim.getWidth() - 1, bim.getHeight() - 1)); - documentOut.addPage(pdPage); - - PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); - PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage); - - // Draw the image with a slight offset and enlarged dimensions - contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2); - contentStream.close(); - } - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - documentOut.save(baos); - documentOut.close(); + PDImageXObject pdImage = LosslessFactory.createFromImage(documentOut, bim); + PDPageContentStream contentStream = new PDPageContentStream(documentOut, pdPage); + + // Draw the image with a slight offset and enlarged dimensions + contentStream.drawImage(pdImage, -1, -1, bim.getWidth() + 2, bim.getHeight() + 2); + contentStream.close(); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + documentOut.save(baos); + documentOut.close(); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_scanned.pdf"; return WebResponseUtils.boasToWebResponse(baos, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java index 027c6240..62783dc4 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/MetadataController.java @@ -19,6 +19,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.MetadataRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -27,7 +28,6 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Misc", description = "Miscellaneous APIs") public class MetadataController { - private String checkUndefined(String entry) { // Check if the string is "undefined" if ("undefined".equals(entry)) { @@ -36,14 +36,16 @@ public class MetadataController { } // Return the original string if it's not "undefined" return entry; - } @PostMapping(consumes = "multipart/form-data", value = "/update-metadata") - @Operation(summary = "Update metadata of a PDF file", - description = "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") - public ResponseEntity metadata(@ModelAttribute MetadataRequest request) throws IOException { - + @Operation( + summary = "Update metadata of a PDF file", + description = + "This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO") + public ResponseEntity metadata(@ModelAttribute MetadataRequest request) + throws IOException { + // Extract PDF file from the request object MultipartFile pdfFile = request.getFileInput(); @@ -61,8 +63,8 @@ public class MetadataController { // Extract additional custom parameters Map allRequestParams = request.getAllRequestParams(); - if(allRequestParams == null) { - allRequestParams = new java.util.HashMap(); + if (allRequestParams == null) { + allRequestParams = new java.util.HashMap(); } // Load the PDF file into a PDDocument PDDocument document = PDDocument.load(pdfFile.getBytes()); @@ -89,7 +91,9 @@ public class MetadataController { } // Remove metadata from the PDF history document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("Metadata")); - document.getDocumentCatalog().getCOSObject().removeItem(COSName.getPDFName("PieceInfo")); + document.getDocumentCatalog() + .getCOSObject() + .removeItem(COSName.getPDFName("PieceInfo")); author = null; creationDate = null; creator = null; @@ -104,9 +108,17 @@ public class MetadataController { for (Entry entry : allRequestParams.entrySet()) { String key = entry.getKey(); // Check if the key is a standard metadata key - if (!key.equalsIgnoreCase("Author") && !key.equalsIgnoreCase("CreationDate") && !key.equalsIgnoreCase("Creator") && !key.equalsIgnoreCase("Keywords") - && !key.equalsIgnoreCase("modificationDate") && !key.equalsIgnoreCase("Producer") && !key.equalsIgnoreCase("Subject") && !key.equalsIgnoreCase("Title") - && !key.equalsIgnoreCase("Trapped") && !key.contains("customKey") && !key.contains("customValue")) { + if (!key.equalsIgnoreCase("Author") + && !key.equalsIgnoreCase("CreationDate") + && !key.equalsIgnoreCase("Creator") + && !key.equalsIgnoreCase("Keywords") + && !key.equalsIgnoreCase("modificationDate") + && !key.equalsIgnoreCase("Producer") + && !key.equalsIgnoreCase("Subject") + && !key.equalsIgnoreCase("Title") + && !key.equalsIgnoreCase("Trapped") + && !key.contains("customKey") + && !key.contains("customValue")) { info.setCustomMetadataValue(key, entry.getValue()); } else if (key.contains("customKey")) { int number = Integer.parseInt(key.replaceAll("\\D", "")); @@ -119,7 +131,8 @@ public class MetadataController { if (creationDate != null && creationDate.length() > 0) { Calendar creationDateCal = Calendar.getInstance(); try { - creationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); + creationDateCal.setTime( + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(creationDate)); } catch (ParseException e) { e.printStackTrace(); } @@ -130,7 +143,8 @@ public class MetadataController { if (modificationDate != null && modificationDate.length() > 0) { Calendar modificationDateCal = Calendar.getInstance(); try { - modificationDateCal.setTime(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); + modificationDateCal.setTime( + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").parse(modificationDate)); } catch (ParseException e) { e.printStackTrace(); } @@ -147,7 +161,8 @@ public class MetadataController { info.setTrapped(trapped); document.setDocumentInformation(info); - return WebResponseUtils.pdfDocToWebResponse(document, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_metadata.pdf"); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java index 5ea1818e..21cf2b1c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OCRController.java @@ -26,6 +26,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -44,14 +45,21 @@ public class OCRController { if (files == null) { return Collections.emptyList(); } - return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) - .filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); + return Arrays.stream(files) + .filter(file -> file.getName().endsWith(".traineddata")) + .map(file -> file.getName().replace(".traineddata", "")) + .filter(lang -> !lang.equalsIgnoreCase("osd")) + .collect(Collectors.toList()); } @PostMapping(consumes = "multipart/form-data", value = "/ocr-pdf") - @Operation(summary = "Process a PDF file with OCR", - description = "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") - public ResponseEntity processPdfWithOCR(@ModelAttribute ProcessPdfWithOcrRequest request) throws IOException, InterruptedException { + @Operation( + summary = "Process a PDF file with OCR", + description = + "This endpoint processes a PDF file using OCR (Optical Character Recognition). Users can specify languages, sidecar, deskew, clean, cleanFinal, ocrType, ocrRenderType, and removeImagesAfter options. Input:PDF Output:PDF Type:SI-Conditional") + public ResponseEntity processPdfWithOCR( + @ModelAttribute ProcessPdfWithOcrRequest request) + throws IOException, InterruptedException { MultipartFile inputFile = request.getFileInput(); List selectedLanguages = request.getLanguages(); Boolean sidecar = request.isSidecar(); @@ -65,16 +73,17 @@ public class OCRController { if (selectedLanguages == null || selectedLanguages.isEmpty()) { throw new IOException("Please select at least one language."); } - - if(!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { + + if (!ocrRenderType.equals("hocr") && !ocrRenderType.equals("sandwich")) { throw new IOException("ocrRenderType wrong"); } - + // Get available Tesseract languages List availableLanguages = getAvailableTesseractLanguages(); // Validate selected languages - selectedLanguages = selectedLanguages.stream().filter(availableLanguages::contains).toList(); + selectedLanguages = + selectedLanguages.stream().filter(availableLanguages::contains).toList(); if (selectedLanguages.isEmpty()) { throw new IOException("None of the selected languages are valid."); @@ -92,8 +101,16 @@ public class OCRController { // Run OCR Command String languageOption = String.join("+", selectedLanguages); - - List command = new ArrayList<>(Arrays.asList("ocrmypdf", "--verbose", "2", "--output-type", "pdf", "--pdf-renderer" , ocrRenderType)); + List command = + new ArrayList<>( + Arrays.asList( + "ocrmypdf", + "--verbose", + "2", + "--output-type", + "pdf", + "--pdf-renderer", + ocrRenderType)); if (sidecar != null && sidecar) { sidecarTextPath = Files.createTempFile("sidecar", ".txt"); @@ -120,42 +137,61 @@ public class OCRController { } } - command.addAll(Arrays.asList("--language", languageOption, tempInputFile.toString(), tempOutputFile.toString())); + command.addAll( + Arrays.asList( + "--language", + languageOption, + tempInputFile.toString(), + tempOutputFile.toString())); // Run CLI command - ProcessExecutorResult result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); - if(result.getRc() != 0 && result.getMessages().contains("multiprocessing/synchronize.py") && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { - command.add("--jobs"); - command.add("1"); - result = ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF).runCommandWithOutputHandling(command); + ProcessExecutorResult result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); + if (result.getRc() != 0 + && result.getMessages().contains("multiprocessing/synchronize.py") + && result.getMessages().contains("OSError: [Errno 38] Function not implemented")) { + command.add("--jobs"); + command.add("1"); + result = + ProcessExecutor.getInstance(ProcessExecutor.Processes.OCR_MY_PDF) + .runCommandWithOutputHandling(command); } - - - // Remove images from the OCR processed PDF if the flag is set to true if (removeImagesAfter != null && removeImagesAfter) { Path tempPdfWithoutImages = Files.createTempFile("output_", "_no_images.pdf"); - List gsCommand = Arrays.asList("gs", "-sDEVICE=pdfwrite", "-dFILTERIMAGE", "-o", tempPdfWithoutImages.toString(), tempOutputFile.toString()); + List gsCommand = + Arrays.asList( + "gs", + "-sDEVICE=pdfwrite", + "-dFILTERIMAGE", + "-o", + tempPdfWithoutImages.toString(), + tempOutputFile.toString()); - ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(gsCommand); + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(gsCommand); tempOutputFile = tempPdfWithoutImages; } // Read the OCR processed PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); // Clean up the temporary files Files.delete(tempInputFile); - + // Return the OCR processed PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.pdf"; if (sidecar != null && sidecar) { // Create a zip file containing both the PDF and the text file - String outputZipFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; + String outputZipFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_OCR.zip"; Path tempZipFile = Files.createTempFile("output_", ".zip"); - try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { + try (ZipOutputStream zipOut = + new ZipOutputStream(new FileOutputStream(tempZipFile.toFile()))) { // Add PDF file to the zip ZipEntry pdfEntry = new ZipEntry(outputFilename); zipOut.putNextEntry(pdfEntry); @@ -177,13 +213,12 @@ public class OCRController { Files.delete(sidecarTextPath); // Return the zip file containing both the PDF and the text file - return WebResponseUtils.bytesToWebResponse(zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + zipBytes, outputZipFilename, MediaType.APPLICATION_OCTET_STREAM); } else { // Return the OCR processed PDF as a response Files.delete(tempOutputFile); return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java index e28f7535..9fe6249c 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/OverlayImageController.java @@ -14,6 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.OverlayImageRequest; import stirling.software.SPDF.utils.PdfUtils; import stirling.software.SPDF.utils.WebResponseUtils; @@ -27,9 +28,9 @@ public class OverlayImageController { @PostMapping(consumes = "multipart/form-data", value = "/add-image") @Operation( - summary = "Overlay image onto a PDF file", - description = "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO" - ) + summary = "Overlay image onto a PDF file", + description = + "This endpoint overlays an image onto a PDF file at the specified coordinates. The image can be overlaid on every page of the PDF if specified. Input:PDF/IMAGE Output:PDF Type:MF-SISO") public ResponseEntity overlayImage(@ModelAttribute OverlayImageRequest request) { MultipartFile pdfFile = request.getFileInput(); MultipartFile imageFile = request.getImageFile(); @@ -41,7 +42,9 @@ public class OverlayImageController { byte[] imageBytes = imageFile.getBytes(); byte[] result = PdfUtils.overlayImage(pdfBytes, imageBytes, x, y, everyPage); - return WebResponseUtils.bytesToWebResponse(result, pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); + return WebResponseUtils.bytesToWebResponse( + result, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_overlayed.pdf"); } catch (IOException e) { logger.error("Failed to add image to PDF", e); return new ResponseEntity<>(HttpStatus.BAD_REQUEST); diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java index 61a1ec97..6c302524 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/PageNumbersController.java @@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest; import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.WebResponseUtils; @@ -33,16 +34,20 @@ public class PageNumbersController { private static final Logger logger = LoggerFactory.getLogger(PageNumbersController.class); @PostMapping(value = "/add-page-numbers", consumes = "multipart/form-data") - @Operation(summary = "Add page numbers to a PDF document", description = "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) throws IOException { + @Operation( + summary = "Add page numbers to a PDF document", + description = + "This operation takes an input PDF file and adds page numbers to it. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addPageNumbers(@ModelAttribute AddPageNumbersRequest request) + throws IOException { MultipartFile file = request.getFileInput(); String customMargin = request.getCustomMargin(); int position = request.getPosition(); int startingNumber = request.getStartingNumber(); String pagesToNumber = request.getPagesToNumber(); String customText = request.getCustomText(); - int pageNumber = startingNumber; - byte[] fileBytes = file.getBytes(); + int pageNumber = startingNumber; + byte[] fileBytes = file.getBytes(); PDDocument document = PDDocument.load(fileBytes); float marginFactor; @@ -58,9 +63,8 @@ public class PageNumbersController { break; case "x-large": marginFactor = 0.075f; - break; - - + break; + default: marginFactor = 0.035f; break; @@ -68,19 +72,29 @@ public class PageNumbersController { float fontSize = 12.0f; PDType1Font font = PDType1Font.HELVETICA; - if(pagesToNumber == null || pagesToNumber.length() == 0) { - pagesToNumber = "all"; + if (pagesToNumber == null || pagesToNumber.length() == 0) { + pagesToNumber = "all"; } - if(customText == null || customText.length() == 0) { - customText = "{n}"; + if (customText == null || customText.length() == 0) { + customText = "{n}"; } - List pagesToNumberList = GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); + List pagesToNumberList = + GeneralUtils.parsePageList(pagesToNumber.split(","), document.getNumberOfPages()); for (int i : pagesToNumberList) { PDPage page = document.getPage(i); PDRectangle pageSize = page.getMediaBox(); - String text = customText != null ? customText.replace("{n}", String.valueOf(pageNumber)).replace("{total}", String.valueOf(document.getNumberOfPages())).replace("{filename}", file.getOriginalFilename().replaceFirst("[.][^.]+$", "")) : String.valueOf(pageNumber); + String text = + customText != null + ? customText + .replace("{n}", String.valueOf(pageNumber)) + .replace("{total}", String.valueOf(document.getNumberOfPages())) + .replace( + "{filename}", + file.getOriginalFilename() + .replaceFirst("[.][^.]+$", "")) + : String.valueOf(pageNumber); float x, y; @@ -88,10 +102,10 @@ public class PageNumbersController { int yGroup = 2 - (position - 1) / 3; switch (xGroup) { - case 0: // left + case 0: // left x = pageSize.getLowerLeftX() + marginFactor * pageSize.getWidth(); break; - case 1: // center + case 1: // center x = pageSize.getLowerLeftX() + (pageSize.getWidth() / 2); break; default: // right @@ -100,10 +114,10 @@ public class PageNumbersController { } switch (yGroup) { - case 0: // bottom + case 0: // bottom y = pageSize.getLowerLeftY() + marginFactor * pageSize.getHeight(); break; - case 1: // middle + case 1: // middle y = pageSize.getLowerLeftY() + (pageSize.getHeight() / 2); break; default: // top @@ -111,7 +125,9 @@ public class PageNumbersController { break; } - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true); contentStream.beginText(); contentStream.setFont(font, fontSize); contentStream.newLineAtOffset(x, y); @@ -126,10 +142,9 @@ public class PageNumbersController { document.save(baos); document.close(); - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", MediaType.APPLICATION_PDF); - + return WebResponseUtils.bytesToWebResponse( + baos.toByteArray(), + file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_numbersAdded.pdf", + MediaType.APPLICATION_PDF); } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java b/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java index f9ae541d..112985a3 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/RepairController.java @@ -17,6 +17,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; @@ -31,11 +32,12 @@ public class RepairController { @PostMapping(consumes = "multipart/form-data", value = "/repair") @Operation( - summary = "Repair a PDF file", - description = "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity repairPdf(@ModelAttribute PDFFile request) throws IOException, InterruptedException { - MultipartFile inputFile = request.getFileInput(); + summary = "Repair a PDF file", + description = + "This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. Input:PDF Output:PDF Type:SISO") + public ResponseEntity repairPdf(@ModelAttribute PDFFile request) + throws IOException, InterruptedException { + MultipartFile inputFile = request.getFileInput(); // Save the uploaded file to a temporary location Path tempInputFile = Files.createTempFile("input_", ".pdf"); inputFile.transferTo(tempInputFile.toFile()); @@ -50,8 +52,9 @@ public class RepairController { command.add("-sDEVICE=pdfwrite"); command.add(tempInputFile.toString()); - - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT).runCommandWithOutputHandling(command); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.GHOSTSCRIPT) + .runCommandWithOutputHandling(command); // Read the optimized PDF file byte[] pdfBytes = Files.readAllBytes(tempOutputFile); @@ -61,8 +64,8 @@ public class RepairController { Files.delete(tempOutputFile); // Return the optimized PDF as a response - String outputFilename = inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; + String outputFilename = + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_repaired.pdf"; return WebResponseUtils.bytesToWebResponse(pdfBytes, outputFilename); } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java index 27431346..ed7852fa 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java +++ b/src/main/java/stirling/software/SPDF/controller/api/misc/ShowJavascript.java @@ -15,47 +15,62 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/misc") @Tag(name = "Misc", description = "Miscellaneous APIs") public class ShowJavascript { private static final Logger logger = LoggerFactory.getLogger(ShowJavascript.class); + @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 extractHeader(@ModelAttribute PDFFile request) throws Exception { - MultipartFile inputFile = request.getFileInput(); + MultipartFile inputFile = request.getFileInput(); String script = ""; try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - - if(document.getDocumentCatalog() != null && document.getDocumentCatalog().getNames() != null) { - PDNameTreeNode jsTree = document.getDocumentCatalog().getNames().getJavaScript(); - - if (jsTree != null) { - Map jsEntries = jsTree.getNames(); - - for (Map.Entry entry : jsEntries.entrySet()) { - String name = entry.getKey(); - PDActionJavaScript jsAction = entry.getValue(); - String jsCodeStr = jsAction.getAction(); - - script += "// File: " + inputFile.getOriginalFilename() + ", Script: " + name + "\n" + jsCodeStr + "\n"; - } - } - } - if (script.isEmpty()) { - script = "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + if (document.getDocumentCatalog() != null + && document.getDocumentCatalog().getNames() != null) { + PDNameTreeNode jsTree = + document.getDocumentCatalog().getNames().getJavaScript(); + + if (jsTree != null) { + Map jsEntries = jsTree.getNames(); + + for (Map.Entry entry : jsEntries.entrySet()) { + String name = entry.getKey(); + PDActionJavaScript jsAction = entry.getValue(); + String jsCodeStr = jsAction.getAction(); + + script += + "// File: " + + inputFile.getOriginalFilename() + + ", Script: " + + name + + "\n" + + jsCodeStr + + "\n"; + } + } } - return WebResponseUtils.bytesToWebResponse(script.getBytes(StandardCharsets.UTF_8), inputFile.getOriginalFilename() + ".js"); + if (script.isEmpty()) { + script = + "PDF '" + inputFile.getOriginalFilename() + "' does not contain Javascript"; + } + + return WebResponseUtils.bytesToWebResponse( + script.getBytes(StandardCharsets.UTF_8), + inputFile.getOriginalFilename() + ".js"); } } - - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java new file mode 100644 index 00000000..6ed5f51d --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/ApiDocService.java @@ -0,0 +1,122 @@ +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.Logger; +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; + +@Service +public class ApiDocService { + + private final Map 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 entity = new HttpEntity<>(headers); + + RestTemplate restTemplate = new RestTemplate(); + ResponseEntity 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 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 diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java index c12fe724..db5e9661 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineController.java @@ -1,56 +1,32 @@ package stirling.software.SPDF.controller.api.pipeline; -import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; 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.Map; -import java.util.stream.Stream; import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; 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.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.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; 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 io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.PipelineConfig; -import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.api.HandleDataRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -59,466 +35,80 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Pipeline", description = "Pipeline APIs") public class PipelineController { - private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); - @Autowired - private ObjectMapper objectMapper; + private static final Logger logger = LoggerFactory.getLogger(PipelineController.class); - final String jsonFileName = "pipelineConfig.json"; - final String watchedFoldersDir = "./pipeline/watchedFolders/"; - final String finishedFoldersDir = "./pipeline/finishedFolders/"; - - @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 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); - } - } + final String watchedFoldersDir = "./pipeline/watchedFolders/"; + final String finishedFoldersDir = "./pipeline/finishedFolders/"; + @Autowired PipelineProcessor processor; - @Autowired - ApplicationProperties applicationProperties; - + @Autowired ApplicationProperties applicationProperties; - private void handleDirectory(Path dir) throws Exception { - 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); - } + @Autowired private ObjectMapper objectMapper; - 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; - } + @PostMapping("/handleData") + public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) + throws JsonMappingException, JsonProcessingException { + if (!Boolean.TRUE.equals(applicationProperties.getSystem().getEnableAlphaFunctionality())) { + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } - // 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; - } + 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); + try { + List inputFiles = processor.generateInputFiles(files); + if (inputFiles == null || inputFiles.size() == 0) { + return null; + } + List outputFiles = processor.runPipelineAgainstFiles(inputFiles, config); + if (outputFiles != null && outputFiles.size() == 1) { + // If there is only one file, return it directly + Resource singleFile = outputFiles.get(0); + InputStream is = singleFile.getInputStream(); + byte[] bytes = new byte[(int) singleFile.contentLength()]; + is.read(bytes); + is.close(); - // 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 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); + logger.info("Returning single file response..."); + return WebResponseUtils.bytesToWebResponse( + bytes, singleFile.getFilename(), MediaType.APPLICATION_OCTET_STREAM); + } else if (outputFiles == null) { + return null; + } - } catch (IOException e) { - e.printStackTrace(); - return; - } - } else { - // If fileInput contains a path, process only this file - files = new File[] { new File(fileInput) }; - } + // Create a ByteArrayOutputStream to hold the zip + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ZipOutputStream zipOut = new ZipOutputStream(baos); - // Prepare the files for processing - List 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()); - } + // Loop through each file and add it to the zip + for (Resource file : outputFiles) { + ZipEntry zipEntry = new ZipEntry(file.getFilename()); + zipOut.putNextEntry(zipEntry); - // Process the files - try { - List 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(); + // Read the file into a byte array + InputStream is = file.getInputStream(); + byte[] bytes = new byte[(int) file.contentLength()]; + is.read(bytes); - String outputFolder = applicationProperties.getAutoPipeline().getOutputFolder(); + // Write the bytes of the file to the zip + zipOut.write(bytes, 0, bytes.length); + zipOut.closeEntry(); - 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)); - } + is.close(); + } - // 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 processFiles(List 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 newOutputFiles = new ArrayList<>(); - boolean hasInputFileType = false; - - for (Resource file : outputFiles) { - if (file.getFilename().endsWith(inputFileExtension)) { - hasInputFileType = true; - MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("fileInput", file); - - Iterator> parameters = parametersNode.fields(); - while (parameters.hasNext()) { - Map.Entry parameter = parameters.next(); - body.add(parameter.getKey(), parameter.getValue().asText()); - } - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.MULTIPART_FORM_DATA); - - HttpEntity> entity = new HttpEntity<>(body, headers); - - RestTemplate restTemplate = new RestTemplate(); - String url = "http://localhost:8080/" + operation; - - ResponseEntity 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 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 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 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 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") - public ResponseEntity handleData(@ModelAttribute HandleDataRequest request) { - MultipartFile[] files = request.getFileInputs(); - String jsonString = request.getJsonString(); - logger.info("Received POST request to /handleData with {} files", files.length); - try { - List outputFiles = handleFiles(files, jsonString); - - if (outputFiles != null && outputFiles.size() == 1) { - // If there is only one file, return it directly - Resource singleFile = outputFiles.get(0); - InputStream is = singleFile.getInputStream(); - byte[] bytes = new byte[(int) singleFile.contentLength()]; - is.read(bytes); - is.close(); - - logger.info("Returning single file response..."); - return WebResponseUtils.bytesToWebResponse(bytes, singleFile.getFilename(), - MediaType.APPLICATION_OCTET_STREAM); - } else if (outputFiles == null) { - return null; - } - - // Create a ByteArrayOutputStream to hold the zip - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ZipOutputStream zipOut = new ZipOutputStream(baos); - - // Loop through each file and add it to the zip - for (Resource file : outputFiles) { - ZipEntry zipEntry = new ZipEntry(file.getFilename()); - zipOut.putNextEntry(zipEntry); - - // Read the file into a byte array - InputStream is = file.getInputStream(); - byte[] bytes = new byte[(int) file.contentLength()]; - is.read(bytes); - - // Write the bytes of the file to the zip - zipOut.write(bytes, 0, bytes.length); - zipOut.closeEntry(); - - is.close(); - } - - zipOut.close(); - - logger.info("Returning zipped file response..."); - return WebResponseUtils.boasToWebResponse(baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); - } catch (Exception e) { - logger.error("Error handling data: ", e); - return null; - } - } - - 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 unzip(byte[] data) throws IOException { - logger.info("Unzipping data of length: {}", data.length); - List 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; - } + zipOut.close(); + logger.info("Returning zipped file response..."); + return WebResponseUtils.boasToWebResponse( + baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM); + } catch (Exception e) { + logger.error("Error handling data: ", e); + return null; + } + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java new file mode 100644 index 00000000..80fdd71c --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -0,0 +1,276 @@ +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 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 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 findJsonFile(Path dir) throws IOException { + try (Stream 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 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 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 prepareFilesForProcessing(File[] files, Path processingDir) + throws IOException { + List 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 filesToProcess, PipelineConfig config, Path dir, Path processingDir) + throws IOException { + try { + List inputFiles = + processor.generateInputFiles(filesToProcess.toArray(new File[0])); + if (inputFiles == null || inputFiles.size() == 0) { + return; + } + List 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 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 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 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); + } + } + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java new file mode 100644 index 00000000..534f3d3b --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineProcessor.java @@ -0,0 +1,346 @@ +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 runPipelineAgainstFiles(List 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 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 newOutputFiles = new ArrayList<>(); + if (!isMultiInputOperation) { + for (Resource file : outputFiles) { + boolean hasInputFileType = false; + if (file.getFilename().endsWith(inputFileExtension)) { + hasInputFileType = true; + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("fileInput", file); + + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } + + ResponseEntity 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 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 body = new LinkedMultiValueMap<>(); + + // Add all matching files to the body + for (Resource file : matchingFiles) { + body.add("fileInput", file); + } + + for (Entry entry : parameters.entrySet()) { + body.add(entry.getKey(), entry.getValue()); + } + + ResponseEntity 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 sendWebRequest(String url, MultiValueMap 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> entity = new HttpEntity<>(body, headers); + + // Make the request to the REST endpoint + return restTemplate.exchange(url, HttpMethod.POST, entity, byte[].class); + } + + private List processOutputFiles( + String operation, + String fileName, + ResponseEntity response, + List 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 generateInputFiles(File[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } + + List 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 generateInputFiles(MultipartFile[] files) throws Exception { + if (files == null || files.length == 0) { + logger.info("No files"); + return null; + } + + List 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 unzip(byte[] data) throws IOException { + logger.info("Unzipping data of length: {}", data.length); + List 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; + } +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java new file mode 100644 index 00000000..1a60441e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/UserServiceInterface.java @@ -0,0 +1,5 @@ +package stirling.software.SPDF.controller.api.pipeline; + +public interface UserServiceInterface { + String getApiKeyForUser(String username); +} diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java index b0e5f2aa..8990c789 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/CertSignController.java @@ -53,6 +53,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -61,198 +62,228 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Security", description = "Security APIs") public class CertSignController { - private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); + private static final Logger logger = LoggerFactory.getLogger(CertSignController.class); - static { - Security.addProvider(new BouncyCastleProvider()); - } + static { + Security.addProvider(new BouncyCastleProvider()); + } - @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") - @Operation(summary = "Sign PDF with a Digital Certificate", description = "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") - public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) throws Exception { - MultipartFile pdf = request.getFileInput(); - String certType = request.getCertType(); - MultipartFile privateKeyFile = request.getPrivateKeyFile(); - MultipartFile certFile = request.getCertFile(); - MultipartFile p12File = request.getP12File(); - String password = request.getPassword(); - Boolean showSignature = request.isShowSignature(); - String reason = request.getReason(); - String location = request.getLocation(); - String name = request.getName(); - Integer pageNumber = request.getPageNumber(); + @PostMapping(consumes = "multipart/form-data", value = "/cert-sign") + @Operation( + summary = "Sign PDF with a Digital Certificate", + description = + "This endpoint accepts a PDF file, a digital certificate and related information to sign the PDF. It then returns the digitally signed PDF file. Input:PDF Output:PDF Type:MF-SISO") + public ResponseEntity signPDFWithCert(@ModelAttribute SignPDFWithCertRequest request) + throws Exception { + MultipartFile pdf = request.getFileInput(); + String certType = request.getCertType(); + MultipartFile privateKeyFile = request.getPrivateKeyFile(); + MultipartFile certFile = request.getCertFile(); + MultipartFile p12File = request.getP12File(); + String password = request.getPassword(); + Boolean showSignature = request.isShowSignature(); + String reason = request.getReason(); + String location = request.getLocation(); + String name = request.getName(); + Integer pageNumber = request.getPageNumber(); - PrivateKey privateKey = null; - X509Certificate cert = null; + PrivateKey privateKey = null; + X509Certificate cert = null; - if (certType != null) { - logger.info("Cert type provided: {}", certType); - switch (certType) { - case "PKCS12": - if (p12File != null) { - KeyStore ks = KeyStore.getInstance("PKCS12"); - ks.load(new ByteArrayInputStream(p12File.getBytes()), password.toCharArray()); - String alias = ks.aliases().nextElement(); - if (!ks.isKeyEntry(alias)) { - throw new IllegalArgumentException("The provided PKCS12 file does not contain a private key."); - } - privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); - cert = (X509Certificate) ks.getCertificate(alias); - } - break; - case "PEM": - if (privateKeyFile != null && certFile != null) { - // Load private key - KeyFactory keyFactory = KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(privateKeyFile.getBytes())) { - privateKey = keyFactory - .generatePrivate(new PKCS8EncodedKeySpec(parsePEM(privateKeyFile.getBytes()))); - } else { - privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); - } + if (certType != null) { + logger.info("Cert type provided: {}", certType); + switch (certType) { + case "PKCS12": + if (p12File != null) { + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load( + new ByteArrayInputStream(p12File.getBytes()), + password.toCharArray()); + String alias = ks.aliases().nextElement(); + if (!ks.isKeyEntry(alias)) { + throw new IllegalArgumentException( + "The provided PKCS12 file does not contain a private key."); + } + privateKey = (PrivateKey) ks.getKey(alias, password.toCharArray()); + cert = (X509Certificate) ks.getCertificate(alias); + } + break; + case "PEM": + if (privateKeyFile != null && certFile != null) { + // Load private key + KeyFactory keyFactory = + KeyFactory.getInstance("RSA", BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(privateKeyFile.getBytes())) { + privateKey = + keyFactory.generatePrivate( + new PKCS8EncodedKeySpec( + parsePEM(privateKeyFile.getBytes()))); + } else { + privateKey = + keyFactory.generatePrivate( + new PKCS8EncodedKeySpec(privateKeyFile.getBytes())); + } - // Load certificate - CertificateFactory certFactory = CertificateFactory.getInstance("X.509", - BouncyCastleProvider.PROVIDER_NAME); - if (isPEM(certFile.getBytes())) { - cert = (X509Certificate) certFactory - .generateCertificate(new ByteArrayInputStream(parsePEM(certFile.getBytes()))); - } else { - cert = (X509Certificate) certFactory - .generateCertificate(new ByteArrayInputStream(certFile.getBytes())); - } - } - break; - } - } - PDSignature signature = new PDSignature(); - signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter - signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); - signature.setName(name); - signature.setLocation(location); - signature.setReason(reason); - signature.setSignDate(Calendar.getInstance()); - - // Load the PDF - try (PDDocument document = PDDocument.load(pdf.getBytes())) { - logger.info("Successfully loaded the provided PDF"); - SignatureOptions signatureOptions = new SignatureOptions(); + // Load certificate + CertificateFactory certFactory = + CertificateFactory.getInstance( + "X.509", BouncyCastleProvider.PROVIDER_NAME); + if (isPEM(certFile.getBytes())) { + cert = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream( + parsePEM(certFile.getBytes()))); + } else { + cert = + (X509Certificate) + certFactory.generateCertificate( + new ByteArrayInputStream(certFile.getBytes())); + } + } + break; + } + } + PDSignature signature = new PDSignature(); + signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE); // default filter + signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_SHA1); + signature.setName(name); + signature.setLocation(location); + signature.setReason(reason); + signature.setSignDate(Calendar.getInstance()); - // If you want to show the signature + // Load the PDF + try (PDDocument document = PDDocument.load(pdf.getBytes())) { + logger.info("Successfully loaded the provided PDF"); + SignatureOptions signatureOptions = new SignatureOptions(); - // ATTEMPT 2 - if (showSignature != null && showSignature) { - PDPage page = document.getPage(pageNumber - 1); + // If you want to show the signature - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm == null) { - acroForm = new PDAcroForm(document); - document.getDocumentCatalog().setAcroForm(acroForm); - } + // ATTEMPT 2 + if (showSignature != null && showSignature) { + PDPage page = document.getPage(pageNumber - 1); - // Create a new signature field and widget + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm == null) { + acroForm = new PDAcroForm(document); + document.getDocumentCatalog().setAcroForm(acroForm); + } - PDSignatureField signatureField = new PDSignatureField(acroForm); - PDAnnotationWidget widget = signatureField.getWidgets().get(0); - PDRectangle rect = new PDRectangle(100, 100, 200, 50); // Define the rectangle size here - widget.setRectangle(rect); - page.getAnnotations().add(widget); + // Create a new signature field and widget -// Set the appearance for the signature field - PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); - PDAppearanceStream appearanceStream = new PDAppearanceStream(document); - appearanceStream.setResources(new PDResources()); - appearanceStream.setBBox(rect); - appearanceDict.setNormalAppearance(appearanceStream); - widget.setAppearance(appearanceDict); + PDSignatureField signatureField = new PDSignatureField(acroForm); + PDAnnotationWidget widget = signatureField.getWidgets().get(0); + PDRectangle rect = + new PDRectangle(100, 100, 200, 50); // Define the rectangle size here + widget.setRectangle(rect); + page.getAnnotations().add(widget); - try (PDPageContentStream contentStream = new PDPageContentStream(document, appearanceStream)) { - contentStream.beginText(); - contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); - contentStream.newLineAtOffset(110, 130); - contentStream.showText("Digitally signed by: " + (name != null ? name : "Unknown")); - contentStream.newLineAtOffset(0, -15); - contentStream.showText("Date: " + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z").format(new Date())); - contentStream.newLineAtOffset(0, -15); - if (reason != null && !reason.isEmpty()) { - contentStream.showText("Reason: " + reason); - contentStream.newLineAtOffset(0, -15); - } - if (location != null && !location.isEmpty()) { - contentStream.showText("Location: " + location); - contentStream.newLineAtOffset(0, -15); - } - contentStream.endText(); - } + // Set the appearance for the signature field + PDAppearanceDictionary appearanceDict = new PDAppearanceDictionary(); + PDAppearanceStream appearanceStream = new PDAppearanceStream(document); + appearanceStream.setResources(new PDResources()); + appearanceStream.setBBox(rect); + appearanceDict.setNormalAppearance(appearanceStream); + widget.setAppearance(appearanceDict); - // Add the widget annotation to the page - page.getAnnotations().add(widget); + try (PDPageContentStream contentStream = + new PDPageContentStream(document, appearanceStream)) { + contentStream.beginText(); + contentStream.setFont(PDType1Font.HELVETICA_BOLD, 12); + contentStream.newLineAtOffset(110, 130); + contentStream.showText( + "Digitally signed by: " + (name != null ? name : "Unknown")); + contentStream.newLineAtOffset(0, -15); + contentStream.showText( + "Date: " + + new SimpleDateFormat("yyyy.MM.dd HH:mm:ss z") + .format(new Date())); + contentStream.newLineAtOffset(0, -15); + if (reason != null && !reason.isEmpty()) { + contentStream.showText("Reason: " + reason); + contentStream.newLineAtOffset(0, -15); + } + if (location != null && !location.isEmpty()) { + contentStream.showText("Location: " + location); + contentStream.newLineAtOffset(0, -15); + } + contentStream.endText(); + } - // Add the signature field to the acroform - acroForm.getFields().add(signatureField); + // Add the widget annotation to the page + page.getAnnotations().add(widget); - // Handle multiple signatures by ensuring a unique field name - String baseFieldName = "Signature"; - String signatureFieldName = baseFieldName; - int suffix = 1; - while (acroForm.getField(signatureFieldName) != null) { - suffix++; - signatureFieldName = baseFieldName + suffix; - } - signatureField.setPartialName(signatureFieldName); - } - - document.addSignature(signature, signatureOptions); - logger.info("Signature added to the PDF document"); - // External signing - ExternalSigningSupport externalSigning = document - .saveIncrementalForExternalSigning(new ByteArrayOutputStream()); + // Add the signature field to the acroform + acroForm.getFields().add(signatureField); - byte[] content = IOUtils.toByteArray(externalSigning.getContent()); + // Handle multiple signatures by ensuring a unique field name + String baseFieldName = "Signature"; + String signatureFieldName = baseFieldName; + int suffix = 1; + while (acroForm.getField(signatureFieldName) != null) { + suffix++; + signatureFieldName = baseFieldName + suffix; + } + signatureField.setPartialName(signatureFieldName); + } - // Using BouncyCastle to sign - CMSTypedData cmsData = new CMSProcessableByteArray(content); + document.addSignature(signature, signatureOptions); + logger.info("Signature added to the PDF document"); + // External signing + ExternalSigningSupport externalSigning = + document.saveIncrementalForExternalSigning(new ByteArrayOutputStream()); - CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); - ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA") - .setProvider(BouncyCastleProvider.PROVIDER_NAME).build(privateKey); + byte[] content = IOUtils.toByteArray(externalSigning.getContent()); - gen.addSignerInfoGenerator(new JcaSignerInfoGeneratorBuilder( - new JcaDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build()) - .build(signer, cert)); + // Using BouncyCastle to sign + CMSTypedData cmsData = new CMSProcessableByteArray(content); - gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); - CMSSignedData signedData = gen.generate(cmsData, false); + CMSSignedDataGenerator gen = new CMSSignedDataGenerator(); + ContentSigner signer = + new JcaContentSignerBuilder("SHA256withRSA") + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build(privateKey); - byte[] cmsSignature = signedData.getEncoded(); - logger.info("About to sign content using BouncyCastle"); - externalSigning.setSignature(cmsSignature); - logger.info("Signature set successfully"); + gen.addSignerInfoGenerator( + new JcaSignerInfoGeneratorBuilder( + new JcaDigestCalculatorProviderBuilder() + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build()) + .build(signer, cert)); - // After setting the signature, return the resultant PDF - try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { - document.save(signedPdfOutput); - return WebResponseUtils.boasToWebResponse(signedPdfOutput, - pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); + gen.addCertificates(new JcaCertStore(Collections.singletonList(cert))); + CMSSignedData signedData = gen.generate(cmsData, false); - } catch (Exception e) { - e.printStackTrace(); - } - } catch (Exception e) { - e.printStackTrace(); - } + byte[] cmsSignature = signedData.getEncoded(); + logger.info("About to sign content using BouncyCastle"); + externalSigning.setSignature(cmsSignature); + logger.info("Signature set successfully"); - return null; - } + // After setting the signature, return the resultant PDF + try (ByteArrayOutputStream signedPdfOutput = new ByteArrayOutputStream()) { + document.save(signedPdfOutput); + return WebResponseUtils.boasToWebResponse( + signedPdfOutput, + pdf.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_signed.pdf"); - private byte[] parsePEM(byte[] content) throws IOException { - PemReader pemReader = new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); - return pemReader.readPemObject().getContent(); - } + } catch (Exception e) { + e.printStackTrace(); + } + } catch (Exception e) { + e.printStackTrace(); + } - private boolean isPEM(byte[] content) { - String contentStr = new String(content); - return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); - } + return null; + } + private byte[] parsePEM(byte[] content) throws IOException { + PemReader pemReader = + new PemReader(new InputStreamReader(new ByteArrayInputStream(content))); + return pemReader.readPemObject().getContent(); + } + + private boolean isPEM(byte[] content) { + String contentStr = new String(content); + return contentStr.contains("-----BEGIN") && contentStr.contains("-----END"); + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java index 791dc736..f0bd8438 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/GetInfoOnPDF.java @@ -72,23 +72,22 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.PDFFile; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") public class GetInfoOnPDF { - - static ObjectMapper objectMapper = new ObjectMapper(); - @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") + static ObjectMapper objectMapper = new ObjectMapper(); + + @PostMapping(consumes = "multipart/form-data", value = "/get-info-on-pdf") @Operation(summary = "Summary here", description = "desc. Input:PDF Output:JSON Type:SISO") - public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) - throws IOException { - MultipartFile inputFile = request.getFileInput(); - try ( - PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); - ) { + public ResponseEntity getPdfInfo(@ModelAttribute PDFFile request) throws IOException { + MultipartFile inputFile = request.getFileInput(); + try (PDDocument pdfBoxDoc = PDDocument.load(inputFile.getInputStream()); ) { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode jsonOutput = objectMapper.createObjectNode(); @@ -100,8 +99,7 @@ public class GetInfoOnPDF { ObjectNode compliancy = objectMapper.createObjectNode(); ObjectNode encryption = objectMapper.createObjectNode(); ObjectNode other = objectMapper.createObjectNode(); - - + metadata.put("Title", info.getTitle()); metadata.put("Author", info.getAuthor()); metadata.put("Subject", info.getSubject()); @@ -111,14 +109,11 @@ public class GetInfoOnPDF { metadata.put("CreationDate", formatDate(info.getCreationDate())); metadata.put("ModificationDate", formatDate(info.getModificationDate())); jsonOutput.set("Metadata", metadata); - - - - + // Total file size of the PDF long fileSizeInBytes = inputFile.getSize(); basicInfo.put("FileSizeInBytes", fileSizeInBytes); - + // Number of words, paragraphs, and images in the entire document String fullText = new PDFTextStripper().getText(pdfBoxDoc); String[] words = fullText.split("\\s+"); @@ -129,8 +124,7 @@ public class GetInfoOnPDF { // Number of characters in the entire document (including spaces and special characters) int charCount = fullText.length(); basicInfo.put("CharacterCount", charCount); - - + // Initialize the flags and types boolean hasCompression = false; String compressionType = "None"; @@ -147,26 +141,21 @@ public class GetInfoOnPDF { } } basicInfo.put("Compression", hasCompression); - if(hasCompression) - basicInfo.put("CompressionType", compressionType); - + if (hasCompression) basicInfo.put("CompressionType", compressionType); + String language = pdfBoxDoc.getDocumentCatalog().getLanguage(); basicInfo.put("Language", language); basicInfo.put("Number of pages", pdfBoxDoc.getNumberOfPages()); - - + PDDocumentCatalog catalog = pdfBoxDoc.getDocumentCatalog(); String pageMode = catalog.getPageMode().name(); - + // Document Information using PDFBox docInfoNode.put("PDF version", pdfBoxDoc.getVersion()); docInfoNode.put("Trapped", info.getTrapped()); - docInfoNode.put("Page Mode", getPageModeDescription(pageMode));; - + docInfoNode.put("Page Mode", getPageModeDescription(pageMode)); + ; - - - PDAcroForm acroForm = pdfBoxDoc.getDocumentCatalog().getAcroForm(); ObjectNode formFieldsNode = objectMapper.createObjectNode(); @@ -177,41 +166,37 @@ public class GetInfoOnPDF { } jsonOutput.set("FormFields", formFieldsNode); - - - - - - //embeed files TODO size - if(catalog.getNames() != null) { - PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); - - ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); - if (efTree != null) { - Map efMap = efTree.getNames(); - if (efMap != null) { - for (Map.Entry entry : efMap.entrySet()) { - ObjectNode embeddedFileNode = objectMapper.createObjectNode(); - embeddedFileNode.put("Name", entry.getKey()); - PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); - if (embeddedFile != null) { - embeddedFileNode.put("FileSize", embeddedFile.getLength()); // size in bytes - } - embeddedFilesArray.add(embeddedFileNode); - } - } - } - other.set("EmbeddedFiles", embeddedFilesArray); + // embeed files TODO size + if (catalog.getNames() != null) { + PDEmbeddedFilesNameTreeNode efTree = catalog.getNames().getEmbeddedFiles(); + + ArrayNode embeddedFilesArray = objectMapper.createArrayNode(); + if (efTree != null) { + Map efMap = efTree.getNames(); + if (efMap != null) { + for (Map.Entry entry : + efMap.entrySet()) { + ObjectNode embeddedFileNode = objectMapper.createObjectNode(); + embeddedFileNode.put("Name", entry.getKey()); + PDEmbeddedFile embeddedFile = entry.getValue().getEmbeddedFile(); + if (embeddedFile != null) { + embeddedFileNode.put( + "FileSize", embeddedFile.getLength()); // size in bytes + } + embeddedFilesArray.add(embeddedFileNode); + } + } + } + other.set("EmbeddedFiles", embeddedFilesArray); } - - - //attachments TODO size + // attachments TODO size ArrayNode attachmentsArray = objectMapper.createArrayNode(); for (PDPage page : pdfBoxDoc.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationFileAttachment) { - PDAnnotationFileAttachment fileAttachmentAnnotation = (PDAnnotationFileAttachment) annotation; + PDAnnotationFileAttachment fileAttachmentAnnotation = + (PDAnnotationFileAttachment) annotation; ObjectNode attachmentNode = objectMapper.createObjectNode(); attachmentNode.put("Name", fileAttachmentAnnotation.getAttachmentName()); @@ -223,7 +208,7 @@ public class GetInfoOnPDF { } other.set("Attachments", attachmentsArray); - //Javascript + // Javascript PDDocumentNameDictionary namesDict = catalog.getNames(); ArrayNode javascriptArray = objectMapper.createArrayNode(); @@ -254,9 +239,9 @@ public class GetInfoOnPDF { } other.set("JavaScript", javascriptArray); - - //TODO size - PDOptionalContentProperties ocProperties = pdfBoxDoc.getDocumentCatalog().getOCProperties(); + // TODO size + PDOptionalContentProperties ocProperties = + pdfBoxDoc.getDocumentCatalog().getOCProperties(); ArrayNode layersArray = objectMapper.createArrayNode(); if (ocProperties != null) { @@ -268,34 +253,38 @@ public class GetInfoOnPDF { } other.set("Layers", layersArray); - - //TODO Security - + // TODO Security - - - PDStructureTreeRoot structureTreeRoot = pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot(); + PDStructureTreeRoot structureTreeRoot = + pdfBoxDoc.getDocumentCatalog().getStructureTreeRoot(); ArrayNode structureTreeArray; - try { - if(structureTreeRoot != null) { - structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); - other.set("StructureTree", structureTreeArray); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - + try { + if (structureTreeRoot != null) { + structureTreeArray = exploreStructureTree(structureTreeRoot.getKids()); + other.set("StructureTree", structureTreeArray); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + boolean isPdfACompliant = checkForStandard(pdfBoxDoc, "PDF/A"); boolean isPdfXCompliant = checkForStandard(pdfBoxDoc, "PDF/X"); boolean isPdfECompliant = checkForStandard(pdfBoxDoc, "PDF/E"); boolean isPdfVTCompliant = checkForStandard(pdfBoxDoc, "PDF/VT"); boolean isPdfUACompliant = checkForStandard(pdfBoxDoc, "PDF/UA"); - boolean isPdfBCompliant = checkForStandard(pdfBoxDoc, "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't an official ISO standard. - boolean isPdfSECCompliant = checkForStandard(pdfBoxDoc, "PDF/SEC"); // This might not be effective since PDF/SEC was under development in 2021. - + boolean isPdfBCompliant = + checkForStandard( + pdfBoxDoc, + "PDF/B"); // If you want to check for PDF/Broadcast, though this isn't + // an official ISO standard. + boolean isPdfSECCompliant = + checkForStandard( + pdfBoxDoc, + "PDF/SEC"); // This might not be effective since PDF/SEC was under + // development in 2021. + compliancy.put("IsPDF/ACompliant", isPdfACompliant); compliancy.put("IsPDF/XCompliant", isPdfXCompliant); compliancy.put("IsPDF/ECompliant", isPdfECompliant); @@ -304,10 +293,6 @@ public class GetInfoOnPDF { compliancy.put("IsPDF/BCompliant", isPdfBCompliant); compliancy.put("IsPDF/SECCompliant", isPdfSECCompliant); - - - - PDOutlineNode root = pdfBoxDoc.getDocumentCatalog().getDocumentOutline(); ArrayNode bookmarksArray = objectMapper.createArrayNode(); @@ -318,33 +303,29 @@ public class GetInfoOnPDF { } other.set("Bookmarks/Outline/TOC", bookmarksArray); - - - PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); - - String xmpString = null; - - if (pdMetadata != null) { - try { - COSInputStream is = pdMetadata.createInputStream(); - DomXmpParser domXmpParser = new DomXmpParser(); - XMPMetadata xmpMeta = domXmpParser.parse(is); - - ByteArrayOutputStream os = new ByteArrayOutputStream(); - new XmpSerializer().serialize(xmpMeta, os, true); - xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); - } catch (XmpParsingException | IOException e) { - e.printStackTrace(); - } - } - - other.put("XMPMetadata", xmpString); + PDMetadata pdMetadata = pdfBoxDoc.getDocumentCatalog().getMetadata(); + + String xmpString = null; + + if (pdMetadata != null) { + try { + COSInputStream is = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(is); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, os, true); + xmpString = new String(os.toByteArray(), StandardCharsets.UTF_8); + } catch (XmpParsingException | IOException e) { + e.printStackTrace(); + } + } + + other.put("XMPMetadata", xmpString); - - if (pdfBoxDoc.isEncrypted()) { - encryption.put("IsEncrypted", true); + encryption.put("IsEncrypted", true); // Retrieve encryption details using getEncryption() PDEncryption pdfEncryption = pdfBoxDoc.getEncryption(); @@ -353,31 +334,30 @@ public class GetInfoOnPDF { AccessPermission ap = pdfBoxDoc.getCurrentAccessPermission(); if (ap != null) { ObjectNode permissionsNode = objectMapper.createObjectNode(); - + permissionsNode.put("CanAssembleDocument", ap.canAssembleDocument()); permissionsNode.put("CanExtractContent", ap.canExtractContent()); - permissionsNode.put("CanExtractForAccessibility", ap.canExtractForAccessibility()); + permissionsNode.put( + "CanExtractForAccessibility", ap.canExtractForAccessibility()); permissionsNode.put("CanFillInForm", ap.canFillInForm()); permissionsNode.put("CanModify", ap.canModify()); permissionsNode.put("CanModifyAnnotations", ap.canModifyAnnotations()); permissionsNode.put("CanPrint", ap.canPrint()); permissionsNode.put("CanPrintDegraded", ap.canPrintDegraded()); - encryption.set("Permissions", permissionsNode); // set the node under "Permissions" - } + encryption.set( + "Permissions", permissionsNode); // set the node under "Permissions" + } // Add other encryption-related properties as needed } else { - encryption.put("IsEncrypted", false); + encryption.put("IsEncrypted", false); } - - - ObjectNode pageInfoParent = objectMapper.createObjectNode(); for (int pageNum = 0; pageNum < pdfBoxDoc.getNumberOfPages(); pageNum++) { ObjectNode pageInfo = objectMapper.createObjectNode(); - // Retrieve the page + // Retrieve the page PDPage page = pdfBoxDoc.getPage(pageNum); // Page-level Information @@ -387,20 +367,20 @@ public class GetInfoOnPDF { float height = mediaBox.getHeight(); ObjectNode sizeInfo = objectMapper.createObjectNode(); - + getDimensionInfo(sizeInfo, width, height); - + sizeInfo.put("Standard Page", getPageSize(width, height)); pageInfo.set("Size", sizeInfo); - + pageInfo.put("Rotation", page.getRotation()); pageInfo.put("Page Orientation", getPageOrientation(width, height)); - // Boxes pageInfo.put("MediaBox", mediaBox.toString()); - // Assuming the following boxes are defined for your document; if not, you may get null values. + // Assuming the following boxes are defined for your document; if not, you may get + // null values. PDRectangle cropBox = page.getCropBox(); pageInfo.put("CropBox", cropBox == null ? "Undefined" : cropBox.toString()); @@ -416,13 +396,13 @@ public class GetInfoOnPDF { // Content Extraction PDFTextStripper textStripper = new PDFTextStripper(); textStripper.setStartPage(pageNum + 1); - textStripper.setEndPage(pageNum +1); + textStripper.setEndPage(pageNum + 1); String pageText = textStripper.getText(pdfBoxDoc); - + pageInfo.put("Text Characters Count", pageText.length()); // // Annotations - + List annotations = page.getAnnotations(); int subtypeCount = 0; @@ -430,10 +410,10 @@ public class GetInfoOnPDF { for (PDAnnotation annotation : annotations) { if (annotation.getSubtype() != null) { - subtypeCount++; // Increase subtype count + subtypeCount++; // Increase subtype count } if (annotation.getContents() != null) { - contentsCount++; // Increase contents count + contentsCount++; // Increase contents count } } @@ -442,26 +422,25 @@ public class GetInfoOnPDF { annotationsObject.put("SubtypeCount", subtypeCount); annotationsObject.put("ContentsCount", contentsCount); pageInfo.set("Annotations", annotationsObject); - - - + // Images (simplified) // This part is non-trivial as images can be embedded in multiple ways in a PDF. // Here is a basic structure to recognize image XObjects on a page. ArrayNode imagesArray = objectMapper.createArrayNode(); PDResources resources = page.getResources(); - for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); if (xObject instanceof PDImageXObject) { PDImageXObject image = (PDImageXObject) xObject; - + ObjectNode imageNode = objectMapper.createObjectNode(); imageNode.put("Width", image.getWidth()); imageNode.put("Height", image.getHeight()); - if(image.getMetadata() != null && image.getMetadata().getFile() != null && image.getMetadata().getFile().getFile() != null) { - imageNode.put("Name", image.getMetadata().getFile().getFile()); + if (image.getMetadata() != null + && image.getMetadata().getFile() != null + && image.getMetadata().getFile().getFile() != null) { + imageNode.put("Name", image.getMetadata().getFile().getFile()); } if (image.getColorSpace() != null) { imageNode.put("ColorSpace", image.getColorSpace().getName()); @@ -472,10 +451,9 @@ public class GetInfoOnPDF { } pageInfo.set("Images", imagesArray); - // Links ArrayNode linksArray = objectMapper.createArrayNode(); - Set uniqueURIs = new HashSet<>(); // To store unique URIs + Set uniqueURIs = new HashSet<>(); // To store unique URIs for (PDAnnotation annotation : annotations) { if (annotation instanceof PDAnnotationLink) { @@ -483,7 +461,7 @@ public class GetInfoOnPDF { if (linkAnnotation.getAction() instanceof PDActionURI) { PDActionURI uriAction = (PDActionURI) linkAnnotation.getAction(); String uri = uriAction.getURI(); - uniqueURIs.add(uri); // Add to set to ensure uniqueness + uniqueURIs.add(uri); // Add to set to ensure uniqueness } } } @@ -495,8 +473,7 @@ public class GetInfoOnPDF { linksArray.add(linkNode); } pageInfo.set("Links", linksArray); - - + // Fonts ArrayNode fontsArray = objectMapper.createArrayNode(); Map uniqueFontsMap = new HashMap<>(); @@ -526,13 +503,13 @@ public class GetInfoOnPDF { fontNode.put("IsNonsymbolic", (flags & 32) != 0); fontNode.put("FontFamily", fontDescriptor.getFontFamily()); - // Font stretch and BBox are not directly available in PDFBox's API, so these are omitted for simplicity + // Font stretch and BBox are not directly available in PDFBox's API, so + // these are omitted for simplicity fontNode.put("FontWeight", fontDescriptor.getFontWeight()); } - // Create a unique key for this font node based on its attributes - String uniqueKey = fontNode.toString(); + String uniqueKey = fontNode.toString(); // Increment count if this font exists, or initialize it if new if (uniqueFontsMap.containsKey(uniqueKey)) { @@ -551,17 +528,7 @@ public class GetInfoOnPDF { } pageInfo.set("Fonts", fontsArray); - - - - - - - - - - - + // Access resources dictionary ArrayNode colorSpacesArray = objectMapper.createArrayNode(); @@ -572,7 +539,7 @@ public class GetInfoOnPDF { PDICCBased iccBased = (PDICCBased) colorSpace; PDStream iccData = iccBased.getPDStream(); byte[] iccBytes = iccData.toByteArray(); - + // TODO: Further decode and analyze the ICC data if needed ObjectNode iccProfileNode = objectMapper.createObjectNode(); iccProfileNode.put("ICC Profile Length", iccBytes.length); @@ -580,14 +547,14 @@ public class GetInfoOnPDF { } } pageInfo.set("Color Spaces & ICC Profiles", colorSpacesArray); - // Other XObjects - Map xObjectCountMap = new HashMap<>(); // To store the count for each type + Map xObjectCountMap = + new HashMap<>(); // To store the count for each type for (COSName name : resources.getXObjectNames()) { PDXObject xObject = resources.getXObject(name); String xObjectType; - + if (xObject instanceof PDImageXObject) { xObjectType = "Image"; } else if (xObject instanceof PDFormXObject) { @@ -597,7 +564,8 @@ public class GetInfoOnPDF { } // Increment the count for this type in the map - xObjectCountMap.put(xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); + xObjectCountMap.put( + xObjectType, xObjectCountMap.getOrDefault(xObjectType, 0) + 1); } // Add the count map to pageInfo (or wherever you want to store it) @@ -606,14 +574,11 @@ public class GetInfoOnPDF { xObjectCountNode.put(entry.getKey(), entry.getValue()); } pageInfo.set("XObjectCounts", xObjectCountNode); - - - ArrayNode multimediaArray = objectMapper.createArrayNode(); for (PDAnnotation annotation : annotations) { - if ("RichMedia".equals(annotation.getSubtype())) { + if ("RichMedia".equals(annotation.getSubtype())) { ObjectNode multimediaNode = objectMapper.createObjectNode(); // Extract details from the annotation as needed multimediaArray.add(multimediaNode); @@ -622,32 +587,29 @@ public class GetInfoOnPDF { pageInfo.set("Multimedia", multimediaArray); - - - pageInfoParent.set("Page " + (pageNum+1), pageInfo); + pageInfoParent.set("Page " + (pageNum + 1), pageInfo); } - jsonOutput.set("BasicInfo", basicInfo); jsonOutput.set("DocumentInfo", docInfoNode); jsonOutput.set("Compliancy", compliancy); jsonOutput.set("Encryption", encryption); jsonOutput.set("Other", other); jsonOutput.set("PerPageInfo", pageInfoParent); - - - + // Save JSON to file - String jsonString = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); - - - - return WebResponseUtils.bytesToWebResponse(jsonString.getBytes(StandardCharsets.UTF_8), "response.json", MediaType.APPLICATION_JSON); - + String jsonString = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonOutput); + + return WebResponseUtils.bytesToWebResponse( + jsonString.getBytes(StandardCharsets.UTF_8), + "response.json", + MediaType.APPLICATION_JSON); + } catch (Exception e) { e.printStackTrace(); } - return null; + return null; } private static void addOutlinesToArray(PDOutlineItem outline, ArrayNode arrayNode) { @@ -665,7 +627,7 @@ public class GetInfoOnPDF { } } - public String getPageOrientation(double width, double height) { + public String getPageOrientation(double width, double height) { if (width > height) { return "Landscape"; } else if (height > width) { @@ -674,6 +636,7 @@ public class GetInfoOnPDF { return "Square"; } } + public String getPageSize(float width, float height) { // Define standard page sizes Map standardSizes = new HashMap<>(); @@ -696,21 +659,22 @@ public class GetInfoOnPDF { return "Custom"; } - private boolean isCloseToSize(float width, float height, float standardWidth, float standardHeight) { + private boolean isCloseToSize( + float width, float height, float standardWidth, float standardHeight) { float tolerance = 1.0f; // You can adjust the tolerance as needed - return Math.abs(width - standardWidth) <= tolerance && Math.abs(height - standardHeight) <= tolerance; + return Math.abs(width - standardWidth) <= tolerance + && Math.abs(height - standardHeight) <= tolerance; } - - public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { + public ObjectNode getDimensionInfo(ObjectNode dimensionInfo, float width, float height) { float ppi = 72; // Points Per Inch - + float widthInInches = width / ppi; float heightInInches = height / ppi; - + float widthInCm = widthInInches * 2.54f; float heightInCm = heightInInches * 2.54f; - + dimensionInfo.put("Width (px)", String.format("%.2f", width)); dimensionInfo.put("Height (px)", String.format("%.2f", height)); dimensionInfo.put("Width (in)", String.format("%.2f", widthInInches)); @@ -720,33 +684,33 @@ public class GetInfoOnPDF { return dimensionInfo; } + public static boolean checkForStandard(PDDocument document, String standardKeyword) { + // Check XMP Metadata + try { + PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); + if (pdMetadata != null) { + COSInputStream metaStream = pdMetadata.createInputStream(); + DomXmpParser domXmpParser = new DomXmpParser(); + XMPMetadata xmpMeta = domXmpParser.parse(metaStream); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + new XmpSerializer().serialize(xmpMeta, baos, true); + String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); -public static boolean checkForStandard(PDDocument document, String standardKeyword) { - // Check XMP Metadata - try { - PDMetadata pdMetadata = document.getDocumentCatalog().getMetadata(); - if (pdMetadata != null) { - COSInputStream metaStream = pdMetadata.createInputStream(); - DomXmpParser domXmpParser = new DomXmpParser(); - XMPMetadata xmpMeta = domXmpParser.parse(metaStream); - - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - new XmpSerializer().serialize(xmpMeta, baos, true); - String xmpString = new String(baos.toByteArray(), StandardCharsets.UTF_8); - - if (xmpString.contains(standardKeyword)) { - return true; + if (xmpString.contains(standardKeyword)) { + return true; + } } + } catch ( + Exception + e) { // Catching general exception for brevity, ideally you'd catch specific + // exceptions. + e.printStackTrace(); } - } catch (Exception e) { // Catching general exception for brevity, ideally you'd catch specific exceptions. - e.printStackTrace(); - } - - return false; -} - + return false; + } + public ArrayNode exploreStructureTree(List nodes) { ArrayNode elementsArray = objectMapper.createArrayNode(); if (nodes != null) { @@ -773,7 +737,6 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo return elementsArray; } - public String getContent(PDStructureElement structureElement) { StringBuilder contentBuilder = new StringBuilder(); @@ -790,8 +753,7 @@ public static boolean checkForStandard(PDDocument document, String standardKeywo return contentBuilder.toString(); } - - + private String formatDate(Calendar calendar) { if (calendar != null) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java index 639b6973..4c3a9517 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/PasswordController.java @@ -16,9 +16,11 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.AddPasswordRequest; import stirling.software.SPDF.model.api.security.PDFPasswordRequest; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") @@ -26,29 +28,31 @@ public class PasswordController { private static final Logger logger = LoggerFactory.getLogger(PasswordController.class); - @PostMapping(consumes = "multipart/form-data", value = "/remove-password") @Operation( - summary = "Remove password from a PDF file", - description = "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO" - ) - public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) throws IOException { + summary = "Remove password from a PDF file", + description = + "This endpoint removes the password from a protected PDF file. Users need to provide the existing password. Input:PDF Output:PDF Type:SISO") + public ResponseEntity removePassword(@ModelAttribute PDFPasswordRequest request) + throws IOException { MultipartFile fileInput = request.getFileInput(); String password = request.getPassword(); - - - + PDDocument document = PDDocument.load(fileInput.getBytes(), password); document.setAllSecurityToBeRemoved(true); - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_password_removed.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_password_removed.pdf"); } @PostMapping(consumes = "multipart/form-data", value = "/add-password") @Operation( - summary = "Add password to a PDF file", - description = "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF" - ) - public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) throws IOException { + summary = "Add password to a PDF file", + description = + "This endpoint adds password protection to a PDF file. Users can specify a set of permissions that should be applied to the file. Input:PDF Output:PDF") + public ResponseEntity addPassword(@ModelAttribute AddPasswordRequest request) + throws IOException { MultipartFile fileInput = request.getFileInput(); String ownerPassword = request.getOwnerPassword(); String password = request.getPassword(); @@ -74,16 +78,19 @@ public class PasswordController { ap.setCanPrintFaithful(!canPrintFaithful); StandardProtectionPolicy spp = new StandardProtectionPolicy(ownerPassword, password, ap); - if(!"".equals(ownerPassword) || !"".equals(password)) { - spp.setEncryptionKeyLength(keyLength); + if (!"".equals(ownerPassword) || !"".equals(password)) { + spp.setEncryptionKeyLength(keyLength); } spp.setPermissions(ap); document.protect(spp); - if("".equals(ownerPassword) && "".equals(password)) - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_permissions.pdf"); - return WebResponseUtils.pdfDocToWebResponse(document, fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); + if ("".equals(ownerPassword) && "".equals(password)) + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_permissions.pdf"); + return WebResponseUtils.pdfDocToWebResponse( + document, + fileInput.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_passworded.pdf"); } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java index 825544dc..79d15065 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/RedactController.java @@ -26,10 +26,12 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.PDFText; import stirling.software.SPDF.model.api.security.RedactPdfRequest; import stirling.software.SPDF.pdf.TextFinder; import stirling.software.SPDF.utils.WebResponseUtils; + @RestController @RequestMapping("/api/v1/security") @Tag(name = "Security", description = "Security APIs") @@ -37,11 +39,13 @@ public class RedactController { private static final Logger logger = LoggerFactory.getLogger(RedactController.class); - @PostMapping(value = "/auto-redact", consumes = "multipart/form-data") - @Operation(summary = "Redacts listOfText in a PDF document", - description = "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") - public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) throws Exception { + @Operation( + summary = "Redacts listOfText in a PDF document", + description = + "This operation takes an input PDF file and redacts the provided listOfText. Input:PDF, Output:PDF, Type:SISO") + public ResponseEntity redactPdf(@ModelAttribute RedactPdfRequest request) + throws Exception { MultipartFile file = request.getFileInput(); String listOfTextString = request.getListOfText(); boolean useRegex = request.isUseRegex(); @@ -49,15 +53,15 @@ public class RedactController { String colorString = request.getRedactColor(); float customPadding = request.getCustomPadding(); boolean convertPDFToImage = request.isConvertPDFToImage(); - - System.out.println(listOfTextString); - String[] listOfText = listOfTextString.split("\n"); + + System.out.println(listOfTextString); + String[] listOfText = listOfTextString.split("\n"); byte[] bytes = file.getBytes(); PDDocument document = PDDocument.load(new ByteArrayInputStream(bytes)); - + Color redactColor; try { - if (!colorString.startsWith("#")) { + if (!colorString.startsWith("#")) { colorString = "#" + colorString; } redactColor = Color.decode(colorString); @@ -66,18 +70,14 @@ public class RedactController { redactColor = Color.BLACK; } - - for (String text : listOfText) { - text = text.trim(); - System.out.println(text); - TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); + text = text.trim(); + System.out.println(text); + TextFinder textFinder = new TextFinder(text, useRegex, wholeWordSearchBool); List foundTexts = textFinder.getTextLocations(document); - redactFoundText(document, foundTexts, customPadding,redactColor); + redactFoundText(document, foundTexts, customPadding, redactColor); } - - - + if (convertPDFToImage) { PDDocument imageDocument = new PDDocument(); PDFRenderer pdfRenderer = new PDFRenderer(document); @@ -97,27 +97,33 @@ public class RedactController { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); document.close(); - + byte[] pdfContent = baos.toByteArray(); - return WebResponseUtils.bytesToWebResponse(pdfContent, + return WebResponseUtils.bytesToWebResponse( + pdfContent, file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_redacted.pdf"); } - - private void redactFoundText(PDDocument document, List blocks, float customPadding, Color redactColor) throws IOException { + private void redactFoundText( + PDDocument document, List blocks, float customPadding, Color redactColor) + throws IOException { var allPages = document.getDocumentCatalog().getPages(); for (PDFText block : blocks) { var page = allPages.get(block.getPageIndex()); - PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true); + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true, true); contentStream.setNonStrokingColor(redactColor); float padding = (block.getY2() - block.getY1()) * 0.3f + customPadding; PDRectangle pageBox = page.getBBox(); - contentStream.addRect(block.getX1(), pageBox.getHeight() - block.getY1() - padding, block.getX2() - block.getX1(), block.getY2() - block.getY1() + 2 * padding); + contentStream.addRect( + block.getX1(), + pageBox.getHeight() - block.getY1() - padding, + block.getX2() - block.getX1(), + block.getY2() - block.getY1() + 2 * padding); contentStream.fill(); contentStream.close(); } } - - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java index dab9d1d7..21a33529 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/SanitizeController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.api.security; + import java.io.IOException; import org.apache.pdfbox.cos.COSDictionary; @@ -28,6 +29,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.SanitizePdfRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -36,59 +38,68 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Security", description = "Security APIs") public class SanitizeController { - @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") - @Operation(summary = "Sanitize a PDF file", - description = "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") - public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) throws IOException { - MultipartFile inputFile = request.getFileInput(); - boolean removeJavaScript = request.isRemoveJavaScript(); - boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); - boolean removeMetadata = request.isRemoveMetadata(); - boolean removeLinks = request.isRemoveLinks(); - boolean removeFonts = request.isRemoveFonts(); + @PostMapping(consumes = "multipart/form-data", value = "/sanitize-pdf") + @Operation( + summary = "Sanitize a PDF file", + description = + "This endpoint processes a PDF file and removes specific elements based on the provided options. Input:PDF Output:PDF Type:SISO") + public ResponseEntity sanitizePDF(@ModelAttribute SanitizePdfRequest request) + throws IOException { + MultipartFile inputFile = request.getFileInput(); + boolean removeJavaScript = request.isRemoveJavaScript(); + boolean removeEmbeddedFiles = request.isRemoveEmbeddedFiles(); + boolean removeMetadata = request.isRemoveMetadata(); + boolean removeLinks = request.isRemoveLinks(); + boolean removeFonts = request.isRemoveFonts(); - try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { - if (removeJavaScript) { - sanitizeJavaScript(document); - } + try (PDDocument document = PDDocument.load(inputFile.getInputStream())) { + if (removeJavaScript) { + sanitizeJavaScript(document); + } - if (removeEmbeddedFiles) { - sanitizeEmbeddedFiles(document); - } + if (removeEmbeddedFiles) { + sanitizeEmbeddedFiles(document); + } - if (removeMetadata) { - sanitizeMetadata(document); - } + if (removeMetadata) { + sanitizeMetadata(document); + } - if (removeLinks) { - sanitizeLinks(document); - } + if (removeLinks) { + sanitizeLinks(document); + } - if (removeFonts) { - sanitizeFonts(document); - } + if (removeFonts) { + sanitizeFonts(document); + } - return WebResponseUtils.pdfDocToWebResponse(document, inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_sanitized.pdf"); - } - } - private void sanitizeJavaScript(PDDocument document) throws IOException { - // Get the root dictionary (catalog) of the PDF - PDDocumentCatalog catalog = document.getDocumentCatalog(); + return WebResponseUtils.pdfDocToWebResponse( + document, + inputFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + + "_sanitized.pdf"); + } + } - // Get the Names dictionary - COSDictionary namesDict = (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); + private void sanitizeJavaScript(PDDocument document) throws IOException { + // Get the root dictionary (catalog) of the PDF + PDDocumentCatalog catalog = document.getDocumentCatalog(); - if (namesDict != null) { - // Get the JavaScript dictionary - COSDictionary javaScriptDict = (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); + // Get the Names dictionary + COSDictionary namesDict = + (COSDictionary) catalog.getCOSObject().getDictionaryObject(COSName.NAMES); - if (javaScriptDict != null) { - // Remove the JavaScript dictionary - namesDict.removeItem(COSName.getPDFName("JavaScript")); - } - } - - for (PDPage page : document.getPages()) { + if (namesDict != null) { + // Get the JavaScript dictionary + COSDictionary javaScriptDict = + (COSDictionary) namesDict.getDictionaryObject(COSName.getPDFName("JavaScript")); + + if (javaScriptDict != null) { + // Remove the JavaScript dictionary + namesDict.removeItem(COSName.getPDFName("JavaScript")); + } + } + + for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { if (annotation instanceof PDAnnotationWidget) { PDAnnotationWidget widget = (PDAnnotationWidget) annotation; @@ -96,33 +107,30 @@ public class SanitizeController { if (action instanceof PDActionJavaScript) { widget.setAction(null); } - } - } - PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); - if (acroForm != null) { - for (PDField field : acroForm.getFields()) { - PDFormFieldAdditionalActions actions = field.getActions(); - if(actions != null) { - if (actions.getC() instanceof PDActionJavaScript) { - actions.setC(null); - } - if (actions.getF() instanceof PDActionJavaScript) { - actions.setF(null); - } - if (actions.getK() instanceof PDActionJavaScript) { - actions.setK(null); - } - if (actions.getV() instanceof PDActionJavaScript) { - actions.setV(null); - } - } - } - } - } - } - - - + } + } + PDAcroForm acroForm = document.getDocumentCatalog().getAcroForm(); + if (acroForm != null) { + for (PDField field : acroForm.getFields()) { + PDFormFieldAdditionalActions actions = field.getActions(); + if (actions != null) { + if (actions.getC() instanceof PDActionJavaScript) { + actions.setC(null); + } + if (actions.getF() instanceof PDActionJavaScript) { + actions.setF(null); + } + if (actions.getK() instanceof PDActionJavaScript) { + actions.setK(null); + } + if (actions.getV() instanceof PDActionJavaScript) { + actions.setV(null); + } + } + } + } + } + } private void sanitizeEmbeddedFiles(PDDocument document) { PDPageTree allPages = document.getPages(); @@ -134,7 +142,6 @@ public class SanitizeController { res.getCOSObject().removeItem(COSName.getPDFName("EmbeddedFiles")); } } - private void sanitizeMetadata(PDDocument document) { PDMetadata metadata = document.getDocumentCatalog().getMetadata(); @@ -143,8 +150,6 @@ public class SanitizeController { } } - - private void sanitizeLinks(PDDocument document) throws IOException { for (PDPage page : document.getPages()) { for (PDAnnotation annotation : page.getAnnotations()) { @@ -163,5 +168,4 @@ public class SanitizeController { page.getResources().getCOSObject().removeItem(COSName.getPDFName("Font")); } } - } diff --git a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java index b19636cd..daee68bf 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/security/WatermarkController.java @@ -30,6 +30,7 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; + import stirling.software.SPDF.model.api.security.AddWatermarkRequest; import stirling.software.SPDF.utils.WebResponseUtils; @@ -38,154 +39,198 @@ import stirling.software.SPDF.utils.WebResponseUtils; @Tag(name = "Security", description = "Security APIs") public class WatermarkController { - @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") - @Operation(summary = "Add watermark to a PDF file", description = "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") - public ResponseEntity addWatermark(@ModelAttribute AddWatermarkRequest request) throws IOException, Exception { - MultipartFile pdfFile = request.getFileInput(); - String watermarkType = request.getWatermarkType(); - String watermarkText = request.getWatermarkText(); - MultipartFile watermarkImage = request.getWatermarkImage(); - String alphabet = request.getAlphabet(); - float fontSize = request.getFontSize(); - float rotation = request.getRotation(); - float opacity = request.getOpacity(); - int widthSpacer = request.getWidthSpacer(); - int heightSpacer = request.getHeightSpacer(); + @PostMapping(consumes = "multipart/form-data", value = "/add-watermark") + @Operation( + summary = "Add watermark to a PDF file", + description = + "This endpoint adds a watermark to a given PDF file. Users can specify the watermark type (text or image), rotation, opacity, width spacer, and height spacer. Input:PDF Output:PDF Type:SISO") + public ResponseEntity addWatermark(@ModelAttribute AddWatermarkRequest request) + throws IOException, Exception { + MultipartFile pdfFile = request.getFileInput(); + String watermarkType = request.getWatermarkType(); + String watermarkText = request.getWatermarkText(); + MultipartFile watermarkImage = request.getWatermarkImage(); + String alphabet = request.getAlphabet(); + float fontSize = request.getFontSize(); + float rotation = request.getRotation(); + float opacity = request.getOpacity(); + int widthSpacer = request.getWidthSpacer(); + int heightSpacer = request.getHeightSpacer(); - // Load the input PDF - PDDocument document = PDDocument.load(pdfFile.getInputStream()); + // Load the input PDF + PDDocument document = PDDocument.load(pdfFile.getInputStream()); - // Create a page in the document - for (PDPage page : document.getPages()) { + // Create a page in the document + for (PDPage page : document.getPages()) { - // Get the page's content stream - PDPageContentStream contentStream = new PDPageContentStream(document, page, - PDPageContentStream.AppendMode.APPEND, true); + // Get the page's content stream + PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true); - // Set transparency - PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); - graphicsState.setNonStrokingAlphaConstant(opacity); - contentStream.setGraphicsStateParameters(graphicsState); + // Set transparency + PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); + graphicsState.setNonStrokingAlphaConstant(opacity); + contentStream.setGraphicsStateParameters(graphicsState); - if (watermarkType.equalsIgnoreCase("text")) { - addTextWatermark(contentStream, watermarkText, document, page, rotation, widthSpacer, heightSpacer, - fontSize, alphabet); - } else if (watermarkType.equalsIgnoreCase("image")) { - addImageWatermark(contentStream, watermarkImage, document, page, rotation, widthSpacer, heightSpacer, - fontSize); - } + if (watermarkType.equalsIgnoreCase("text")) { + addTextWatermark( + contentStream, + watermarkText, + document, + page, + rotation, + widthSpacer, + heightSpacer, + fontSize, + alphabet); + } else if (watermarkType.equalsIgnoreCase("image")) { + addImageWatermark( + contentStream, + watermarkImage, + document, + page, + rotation, + widthSpacer, + heightSpacer, + fontSize); + } - // Close the content stream - contentStream.close(); - } + // Close the content stream + contentStream.close(); + } - return WebResponseUtils.pdfDocToWebResponse(document, - pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); - } + return WebResponseUtils.pdfDocToWebResponse( + document, + pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_watermarked.pdf"); + } - private void addTextWatermark(PDPageContentStream contentStream, String watermarkText, PDDocument document, - PDPage page, float rotation, int widthSpacer, int heightSpacer, float fontSize, String alphabet) throws IOException { - String resourceDir = ""; - PDFont font = PDType1Font.HELVETICA_BOLD; - switch (alphabet) { - case "arabic": - resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; - break; - case "japanese": - resourceDir = "static/fonts/Meiryo.ttf"; - break; - case "korean": - resourceDir = "static/fonts/malgun.ttf"; - break; - case "chinese": - resourceDir = "static/fonts/SimSun.ttf"; - break; - case "roman": - default: - resourceDir = "static/fonts/NotoSans-Regular.ttf"; - break; - } + private void addTextWatermark( + PDPageContentStream contentStream, + String watermarkText, + PDDocument document, + PDPage page, + float rotation, + int widthSpacer, + int heightSpacer, + float fontSize, + String alphabet) + throws IOException { + String resourceDir = ""; + PDFont font = PDType1Font.HELVETICA_BOLD; + switch (alphabet) { + case "arabic": + resourceDir = "static/fonts/NotoSansArabic-Regular.ttf"; + break; + case "japanese": + resourceDir = "static/fonts/Meiryo.ttf"; + break; + case "korean": + resourceDir = "static/fonts/malgun.ttf"; + break; + case "chinese": + resourceDir = "static/fonts/SimSun.ttf"; + break; + case "roman": + default: + resourceDir = "static/fonts/NotoSans-Regular.ttf"; + break; + } - - if(!resourceDir.equals("")) { + if (!resourceDir.equals("")) { ClassPathResource classPathResource = new ClassPathResource(resourceDir); String fileExtension = resourceDir.substring(resourceDir.lastIndexOf(".")); File tempFile = File.createTempFile("NotoSansFont", fileExtension); - try (InputStream is = classPathResource.getInputStream(); FileOutputStream os = new FileOutputStream(tempFile)) { + try (InputStream is = classPathResource.getInputStream(); + FileOutputStream os = new FileOutputStream(tempFile)) { IOUtils.copy(is, os); } - + font = PDType0Font.load(document, tempFile); tempFile.deleteOnExit(); } - - contentStream.setFont(font, fontSize); - contentStream.setNonStrokingColor(Color.LIGHT_GRAY); - // Set size and location of text watermark - float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; - float watermarkHeight = heightSpacer + fontSize; - float pageWidth = page.getMediaBox().getWidth(); - float pageHeight = page.getMediaBox().getHeight(); - int watermarkRows = (int) (pageHeight / watermarkHeight + 1); - int watermarkCols = (int) (pageWidth / watermarkWidth + 1); + contentStream.setFont(font, fontSize); + contentStream.setNonStrokingColor(Color.LIGHT_GRAY); - // Add the text watermark - for (int i = 0; i < watermarkRows; i++) { - for (int j = 0; j < watermarkCols; j++) { - contentStream.beginText(); - contentStream.setTextMatrix(Matrix.getRotateInstance((float) Math.toRadians(rotation), - j * watermarkWidth, i * watermarkHeight)); - contentStream.showText(watermarkText); - contentStream.endText(); - } - } - } + // Set size and location of text watermark + float watermarkWidth = widthSpacer + font.getStringWidth(watermarkText) * fontSize / 1000; + float watermarkHeight = heightSpacer + fontSize; + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + int watermarkRows = (int) (pageHeight / watermarkHeight + 1); + int watermarkCols = (int) (pageWidth / watermarkWidth + 1); - private void addImageWatermark(PDPageContentStream contentStream, MultipartFile watermarkImage, PDDocument document, PDPage page, float rotation, - int widthSpacer, int heightSpacer, float fontSize) throws IOException { + // Add the text watermark + for (int i = 0; i < watermarkRows; i++) { + for (int j = 0; j < watermarkCols; j++) { + contentStream.beginText(); + contentStream.setTextMatrix( + Matrix.getRotateInstance( + (float) Math.toRadians(rotation), + j * watermarkWidth, + i * watermarkHeight)); + contentStream.showText(watermarkText); + contentStream.endText(); + } + } + } -// Load the watermark image -BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); + private void addImageWatermark( + PDPageContentStream contentStream, + MultipartFile watermarkImage, + PDDocument document, + PDPage page, + float rotation, + int widthSpacer, + int heightSpacer, + float fontSize) + throws IOException { -// Compute width based on original aspect ratio -float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); + // Load the watermark image + BufferedImage image = ImageIO.read(watermarkImage.getInputStream()); -// Desired physical height (in PDF points) -float desiredPhysicalHeight = fontSize ; + // Compute width based on original aspect ratio + float aspectRatio = (float) image.getWidth() / (float) image.getHeight(); -// Desired physical width based on the aspect ratio -float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; + // Desired physical height (in PDF points) + float desiredPhysicalHeight = fontSize; -// Convert the BufferedImage to PDImageXObject -PDImageXObject xobject = LosslessFactory.createFromImage(document, image); + // Desired physical width based on the aspect ratio + float desiredPhysicalWidth = desiredPhysicalHeight * aspectRatio; -// Calculate the number of rows and columns for watermarks -float pageWidth = page.getMediaBox().getWidth(); -float pageHeight = page.getMediaBox().getHeight(); -int watermarkRows = (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer)); -int watermarkCols = (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer)); + // Convert the BufferedImage to PDImageXObject + PDImageXObject xobject = LosslessFactory.createFromImage(document, image); -for (int i = 0; i < watermarkRows; i++) { -for (int j = 0; j < watermarkCols; j++) { -float x = j * (desiredPhysicalWidth + widthSpacer); -float y = i * (desiredPhysicalHeight + heightSpacer); + // Calculate the number of rows and columns for watermarks + float pageWidth = page.getMediaBox().getWidth(); + float pageHeight = page.getMediaBox().getHeight(); + int watermarkRows = + (int) ((pageHeight + heightSpacer) / (desiredPhysicalHeight + heightSpacer)); + int watermarkCols = + (int) ((pageWidth + widthSpacer) / (desiredPhysicalWidth + widthSpacer)); -// Save the graphics state -contentStream.saveGraphicsState(); + for (int i = 0; i < watermarkRows; i++) { + for (int j = 0; j < watermarkCols; j++) { + float x = j * (desiredPhysicalWidth + widthSpacer); + float y = i * (desiredPhysicalHeight + heightSpacer); -// Create rotation matrix and rotate -contentStream.transform(Matrix.getTranslateInstance(x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); -contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); -contentStream.transform(Matrix.getTranslateInstance(-desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2)); - -// Draw the image and restore the graphics state -contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); -contentStream.restoreGraphicsState(); -} - -} - - } + // Save the graphics state + contentStream.saveGraphicsState(); + // Create rotation matrix and rotate + contentStream.transform( + Matrix.getTranslateInstance( + x + desiredPhysicalWidth / 2, y + desiredPhysicalHeight / 2)); + contentStream.transform(Matrix.getRotateInstance(Math.toRadians(rotation), 0, 0)); + contentStream.transform( + Matrix.getTranslateInstance( + -desiredPhysicalWidth / 2, -desiredPhysicalHeight / 2)); + + // Draw the image and restore the graphics state + contentStream.drawImage(xobject, 0, 0, desiredPhysicalWidth, desiredPhysicalHeight); + contentStream.restoreGraphicsState(); + } + } + } } diff --git a/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java b/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java index 5330c3ad..e2ed3ca0 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java +++ b/src/main/java/stirling/software/SPDF/controller/api/strippers/PDFTableStripper.java @@ -24,91 +24,79 @@ import org.apache.pdfbox.text.PDFTextStripperByArea; import org.apache.pdfbox.text.TextPosition; /** + * Class to extract tabular data from a PDF. Works by making a first pass of the page to group all + * nearby text items together, and then inferring a 2D grid from these regions. Each table cell is + * then extracted using a PDFTextStripperByArea object. * - * Class to extract tabular data from a PDF. - * Works by making a first pass of the page to group all nearby text items - * together, and then inferring a 2D grid from these regions. Each table cell - * is then extracted using a PDFTextStripperByArea object. + *

Works best when headers are included in the detected region, to ensure representative text in + * every column. * - * Works best when - * headers are included in the detected region, to ensure representative text - * in every column. - * - * Based upon DrawPrintTextLocations PDFBox example + *

Based upon DrawPrintTextLocations PDFBox example * (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java) * * @author Beldaz */ -public class PDFTableStripper extends PDFTextStripper -{ +public class PDFTableStripper extends PDFTextStripper { /** * This will print the documents data, for each table cell. * * @param args The command line arguments. - * * @throws IOException If there is an error parsing the document. */ /* * Used in methods derived from DrawPrintTextLocations */ private AffineTransform flipAT; + private AffineTransform rotateAT; - /** - * Regions updated by calls to writeString - */ + /** Regions updated by calls to writeString */ private Set boxes; // Border to allow when finding intersections private double dx = 1.0; // This value works for me, feel free to tweak (or add setter) private double dy = 0.000; // Rows of text tend to overlap, so need to extend - /** - * Region in which to find table (otherwise whole page) - */ + /** Region in which to find table (otherwise whole page) */ private Rectangle2D regionArea; - /** - * Number of rows in inferred table - */ - private int nRows=0; + /** Number of rows in inferred table */ + private int nRows = 0; - /** - * Number of columns in inferred table - */ - private int nCols=0; + /** Number of columns in inferred table */ + private int nCols = 0; - /** - * This is the object that does the text extraction - */ + /** This is the object that does the text extraction */ private PDFTextStripperByArea regionStripper; /** * 1D intervals - used for calculateTableRegions() - * @author Beldaz * + * @author Beldaz */ public static class Interval { double start; double end; + public Interval(double start, double end) { - this.start=start; this.end = end; + this.start = start; + this.end = end; } + public void add(Interval col) { - if(col.startend) - end = col.end; + if (col.start < start) start = col.start; + if (col.end > end) end = col.end; } + public static void addTo(Interval x, LinkedList columns) { int p = 0; Iterator it = columns.iterator(); // Find where x should go - while(it.hasNext()) { + while (it.hasNext()) { Interval col = it.next(); - if(x.end>=col.start) { - if(x.start<=col.end) { // overlaps + if (x.end >= col.start) { + if (x.start <= col.end) { // overlaps x.add(col); it.remove(); } @@ -116,30 +104,26 @@ public class PDFTableStripper extends PDFTextStripper } ++p; } - while(it.hasNext()) { + while (it.hasNext()) { Interval col = it.next(); - if(x.start>col.end) - break; + if (x.start > col.end) break; x.add(col); it.remove(); } columns.add(p, x); } - } - /** * Instantiate a new PDFTableStripper object. * * @param document * @throws IOException If there is an error loading the properties. */ - public PDFTableStripper() throws IOException - { + public PDFTableStripper() throws IOException { super.setShouldSeparateByBeads(false); regionStripper = new PDFTextStripperByArea(); - regionStripper.setSortByPosition( true ); + regionStripper.setSortByPosition(true); } /** @@ -147,18 +131,15 @@ public class PDFTableStripper extends PDFTextStripper * * @param rect The rectangle area to retrieve the text from. */ - public void setRegion(Rectangle2D rect ) - { + public void setRegion(Rectangle2D rect) { regionArea = rect; } - public int getRows() - { + public int getRows() { return nRows; } - public int getColumns() - { + public int getColumns() { return nCols; } @@ -167,13 +148,11 @@ public class PDFTableStripper extends PDFTextStripper * * @return The text that was identified in that region. */ - public String getText(int row, int col) - { - return regionStripper.getTextForRegion("el"+col+"x"+row); + public String getText(int row, int col) { + return regionStripper.getTextForRegion("el" + col + "x" + row); } - public void extractTable(PDPage pdPage) throws IOException - { + public void extractTable(PDPage pdPage) throws IOException { setStartPage(getCurrentPageNo()); setEndPage(getCurrentPageNo()); @@ -186,11 +165,9 @@ public class PDFTableStripper extends PDFTextStripper // page may be rotated rotateAT = new AffineTransform(); int rotation = pdPage.getRotation(); - if (rotation != 0) - { + if (rotation != 0) { PDRectangle mediaBox = pdPage.getMediaBox(); - switch (rotation) - { + switch (rotation) { case 90: rotateAT.translate(mediaBox.getHeight(), 0); break; @@ -213,11 +190,12 @@ public class PDFTableStripper extends PDFTextStripper Rectangle2D[][] regions = calculateTableRegions(); -// System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions"); - for(int i=0; i columns = new LinkedList(); LinkedList rows = new LinkedList(); - for(Rectangle2D box: boxes) { + for (Rectangle2D box : boxes) { Interval x = new Interval(box.getMinX(), box.getMaxX()); Interval y = new Interval(box.getMinY(), box.getMaxY()); @@ -249,12 +227,17 @@ public class PDFTableStripper extends PDFTextStripper nRows = rows.size(); nCols = columns.size(); Rectangle2D[][] regions = new Rectangle2D[nCols][nRows]; - int i=0; + int i = 0; // Label regions from top left, rather than the transformed orientation - for(Interval column: columns) { - int j=0; - for(Interval row: rows) { - regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start); + for (Interval column : columns) { + int j = 0; + for (Interval row : rows) { + regions[nCols - i - 1][nRows - j - 1] = + new Rectangle2D.Double( + column.start, + row.start, + column.end - column.start, + row.end - row.start); ++j; } ++i; @@ -264,18 +247,15 @@ public class PDFTableStripper extends PDFTextStripper } /** - * Register each character's bounding box, updating boxes field to maintain - * a list of all distinct groups of characters. + * Register each character's bounding box, updating boxes field to maintain a list of all + * distinct groups of characters. * - * Overrides the default functionality of PDFTextStripper. - * Most of this is taken from DrawPrintTextLocations.java, with extra steps - * at end of main loop + *

Overrides the default functionality of PDFTextStripper. Most of this is taken from + * DrawPrintTextLocations.java, with extra steps at end of main loop */ @Override - protected void writeString(String string, List textPositions) throws IOException - { - for (TextPosition text : textPositions) - { + protected void writeString(String string, List textPositions) throws IOException { + for (TextPosition text : textPositions) { // glyph space -> user space // note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix AffineTransform at = text.getTextMatrix().createAffineTransform(); @@ -283,37 +263,35 @@ public class PDFTableStripper extends PDFTextStripper BoundingBox bbox = font.getBoundingBox(); // advance width, bbox height (glyph space) - float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars - Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); + float xadvance = + font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars + Rectangle2D.Float rect = + new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); - if (font instanceof PDType3Font) - { + if (font instanceof PDType3Font) { // bbox and font matrix are unscaled at.concatenate(font.getFontMatrix().createAffineTransform()); - } - else - { + } else { // bbox and font matrix are already scaled to 1000 - at.scale(1/1000f, 1/1000f); + at.scale(1 / 1000f, 1 / 1000f); } Shape s = at.createTransformedShape(rect); s = flipAT.createTransformedShape(s); s = rotateAT.createTransformedShape(s); - // // Merge character's bounding box with boxes field // Rectangle2D bounds = s.getBounds2D(); // Pad sides to detect almost touching boxes Rectangle2D hitbox = bounds.getBounds2D(); - hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy); - hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy); + hitbox.add(bounds.getMinX() - dx, bounds.getMinY() - dy); + hitbox.add(bounds.getMaxX() + dx, bounds.getMaxY() + dy); // Find all overlapping boxes List intersectList = new ArrayList(); - for(Rectangle2D box: boxes) { - if(box.intersects(hitbox)) { + for (Rectangle2D box : boxes) { + if (box.intersects(hitbox)) { intersectList.add(box); } } @@ -321,38 +299,30 @@ public class PDFTableStripper extends PDFTextStripper // Combine all touching boxes and update // (NOTE: Potentially this could leave some overlapping boxes un-merged, // but it's sufficient for now and get's fixed up in calculateTableRegions) - for(Rectangle2D box: intersectList) { + for (Rectangle2D box : intersectList) { bounds.add(box); boxes.remove(box); } boxes.add(bounds); - } - } /** - * This method does nothing in this derived class, because beads and regions are incompatible. Beads are - * ignored when stripping by area. + * This method does nothing in this derived class, because beads and regions are incompatible. + * Beads are ignored when stripping by area. * * @param aShouldSeparateByBeads The new grouping of beads. */ @Override - public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) - { - } + public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) {} - /** - * Adapted from PDFTextStripperByArea - * {@inheritDoc} - */ + /** Adapted from PDFTextStripperByArea {@inheritDoc} */ @Override - protected void processTextPosition( TextPosition text ) - { - if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) { + protected void processTextPosition(TextPosition text) { + if (regionArea != null && !regionArea.contains(text.getX(), text.getY())) { // skip character } else { - super.processTextPosition( text ); + super.processTextPosition(text); } } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java index cba7ebb5..614dd8a0 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/AccountWebController.java @@ -1,4 +1,6 @@ package stirling.software.SPDF.controller.web; + +import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -14,122 +16,140 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.oas.annotations.tags.Tag; + 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.repository.UserRepository; + @Controller @Tag(name = "Account Security", description = "Account Security APIs") public class AccountWebController { - - @GetMapping("/login") - public String login(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication != null && authentication.isAuthenticated()) { + @GetMapping("/login") + public String login(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication != null && authentication.isAuthenticated()) { return "redirect:/"; } - - if (request.getParameter("error") != null) { - model.addAttribute("error", request.getParameter("error")); - } - if (request.getParameter("logout") != null) { + if (request.getParameter("error") != null) { - model.addAttribute("logoutMessage", "You have been logged out."); - } - - return "login"; - } - @Autowired - private UserRepository userRepository; // Assuming you have a repository for user operations + model.addAttribute("error", request.getParameter("error")); + } + if (request.getParameter("logout") != null) { + model.addAttribute("logoutMessage", "You have been logged out."); + } - @PreAuthorize("hasRole('ROLE_ADMIN')") - @GetMapping("/addUsers") - public String showAddUserForm(Model model, Authentication authentication) { - List allUsers = userRepository.findAll(); - model.addAttribute("users", allUsers); - model.addAttribute("currentUsername", authentication.getName()); - return "addUsers"; - } + return "login"; + } - - - @GetMapping("/account") - public String account(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { + @Autowired + private UserRepository userRepository; // Assuming you have a repository for user operations + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @GetMapping("/addUsers") + public String showAddUserForm(Model model, Authentication authentication) { + List allUsers = userRepository.findAll(); + Iterator 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("currentUsername", authentication.getName()); + return "addUsers"; + } + + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + @GetMapping("/account") + public String account(HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); - if (principal instanceof UserDetails) { - // Cast the principal object to UserDetails - UserDetails userDetails = (UserDetails) principal; + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; - // Retrieve username and other attributes - String username = userDetails.getUsername(); + // Retrieve username and other attributes + String username = userDetails.getUsername(); - // Fetch user details from the database - Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists - if (!user.isPresent()) { - // Handle error appropriately - return "redirect:/error"; // Example redirection in case of error - } + // Fetch user details from the database + Optional user = + userRepository.findByUsername( + username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } - // Convert settings map to JSON string - ObjectMapper objectMapper = new ObjectMapper(); - String settingsJson; - try { - settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); - } catch (JsonProcessingException e) { - // Handle JSON conversion error - e.printStackTrace(); - return "redirect:/error"; // Example redirection in case of error - } + // Convert settings map to JSON string + ObjectMapper objectMapper = new ObjectMapper(); + String settingsJson; + try { + settingsJson = objectMapper.writeValueAsString(user.get().getSettings()); + } catch (JsonProcessingException e) { + // Handle JSON conversion error + e.printStackTrace(); + return "redirect:/error"; // Example redirection in case of error + } - // Add attributes to the model - model.addAttribute("username", username); - model.addAttribute("role", user.get().getRolesAsString()); - model.addAttribute("settings", settingsJson); - model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); - } - } else { - return "redirect:/"; - } - return "account"; - } - - - - @GetMapping("/change-creds") - public String changeCreds(HttpServletRequest request, Model model, Authentication authentication) { - if (authentication == null || !authentication.isAuthenticated()) { + // Add attributes to the model + model.addAttribute("username", username); + model.addAttribute("role", user.get().getRolesAsString()); + model.addAttribute("settings", settingsJson); + model.addAttribute("changeCredsFlag", user.get().isFirstLogin()); + } + } else { return "redirect:/"; } - if (authentication != null && authentication.isAuthenticated()) { - Object principal = authentication.getPrincipal(); + return "account"; + } - if (principal instanceof UserDetails) { - // Cast the principal object to UserDetails - UserDetails userDetails = (UserDetails) principal; + @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") + @GetMapping("/change-creds") + public String changeCreds( + HttpServletRequest request, Model model, Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return "redirect:/"; + } + if (authentication != null && authentication.isAuthenticated()) { + Object principal = authentication.getPrincipal(); - // Retrieve username and other attributes - String username = userDetails.getUsername(); + if (principal instanceof UserDetails) { + // Cast the principal object to UserDetails + UserDetails userDetails = (UserDetails) principal; - // Fetch user details from the database - Optional user = userRepository.findByUsername(username); // Assuming findByUsername method exists - if (!user.isPresent()) { - // Handle error appropriately - return "redirect:/error"; // Example redirection in case of error - } - // Add attributes to the model - model.addAttribute("username", username); - } - } else { - return "redirect:/"; - } - return "change-creds"; - } - - + // Retrieve username and other attributes + String username = userDetails.getUsername(); + + // Fetch user details from the database + Optional user = + userRepository.findByUsername( + username); // Assuming findByUsername method exists + if (!user.isPresent()) { + // Handle error appropriately + return "redirect:/error"; // Example redirection in case of error + } + // Add attributes to the model + model.addAttribute("username", username); + } + } else { + return "redirect:/"; + } + return "change-creds"; + } } diff --git a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java index 3bad71c9..b34bac3c 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/ConverterWebController.java @@ -25,14 +25,14 @@ public class ConverterWebController { model.addAttribute("currentPage", "html-to-pdf"); return "convert/html-to-pdf"; } + @GetMapping("/markdown-to-pdf") @Hidden public String convertMarkdownToPdfForm(Model model) { model.addAttribute("currentPage", "markdown-to-pdf"); return "convert/markdown-to-pdf"; } - - + @GetMapping("/url-to-pdf") @Hidden public String convertURLToPdfForm(Model model) { @@ -40,25 +40,22 @@ public class ConverterWebController { return "convert/url-to-pdf"; } - @GetMapping("/pdf-to-img") @Hidden public String pdfToimgForm(Model model) { model.addAttribute("currentPage", "pdf-to-img"); return "convert/pdf-to-img"; } - + @GetMapping("/file-to-pdf") @Hidden public String convertToPdfForm(Model model) { model.addAttribute("currentPage", "file-to-pdf"); return "convert/file-to-pdf"; } - - - //PDF TO...... - + // PDF TO...... + @GetMapping("/pdf-to-html") @Hidden public ModelAndView pdfToHTML() { @@ -107,7 +104,6 @@ public class ConverterWebController { return modelAndView; } - @GetMapping("/pdf-to-pdfa") @Hidden public String pdfToPdfAForm(Model model) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java index 1542b7ac..c683868e 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -1,5 +1,6 @@ package stirling.software.SPDF.controller.web; +import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -31,57 +32,63 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Controller @Tag(name = "General", description = "General APIs") public class GeneralWebController { - - - - @GetMapping("/pipeline") - @Hidden - public String pipelineForm(Model model) { - model.addAttribute("currentPage", "pipeline"); + @GetMapping("/pipeline") + @Hidden + public String pipelineForm(Model model) { + model.addAttribute("currentPage", "pipeline"); - List pipelineConfigs = new ArrayList<>(); - try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { - List jsonFiles = paths - .filter(Files::isRegularFile) - .filter(p -> p.toString().endsWith(".json")) - .collect(Collectors.toList()); + List pipelineConfigs = new ArrayList<>(); + List> pipelineConfigsWithNames = new ArrayList<>(); - for (Path jsonFile : jsonFiles) { - String content = Files.readString(jsonFile, StandardCharsets.UTF_8); - pipelineConfigs.add(content); - } - List> pipelineConfigsWithNames = new ArrayList<>(); - for (String config : pipelineConfigs) { - Map jsonContent = new ObjectMapper().readValue(config, new TypeReference>(){}); + if (new File("./pipeline/defaultWebUIConfigs/").exists()) { + try (Stream paths = Files.walk(Paths.get("./pipeline/defaultWebUIConfigs/"))) { + List jsonFiles = + paths.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".json")) + .collect(Collectors.toList()); - String name = (String) jsonContent.get("name"); - Map configWithName = new HashMap<>(); - configWithName.put("json", config); - configWithName.put("name", name); - pipelineConfigsWithNames.add(configWithName); - } - model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + for (Path jsonFile : jsonFiles) { + String content = Files.readString(jsonFile, StandardCharsets.UTF_8); + pipelineConfigs.add(content); + } - } catch (IOException e) { - e.printStackTrace(); - } + for (String config : pipelineConfigs) { + Map jsonContent = + new ObjectMapper() + .readValue(config, new TypeReference>() {}); - model.addAttribute("pipelineConfigs", pipelineConfigs); + String name = (String) jsonContent.get("name"); + Map configWithName = new HashMap<>(); + configWithName.put("json", config); + configWithName.put("name", name); + pipelineConfigsWithNames.add(configWithName); + } - return "pipeline"; - } + } catch (IOException e) { + e.printStackTrace(); + } + } + if (pipelineConfigsWithNames.size() == 0) { + Map configWithName = new HashMap<>(); + configWithName.put("json", ""); + configWithName.put("name", "No preloaded configs found"); + pipelineConfigsWithNames.add(configWithName); + } + model.addAttribute("pipelineConfigsWithNames", pipelineConfigsWithNames); + + model.addAttribute("pipelineConfigs", pipelineConfigs); + + return "pipeline"; + } - - - @GetMapping("/merge-pdfs") @Hidden public String mergePdfForm(Model model) { model.addAttribute("currentPage", "merge-pdfs"); return "merge-pdfs"; } - + @GetMapping("/split-pdf-by-sections") @Hidden public String splitPdfBySections(Model model) { @@ -95,57 +102,56 @@ public class GeneralWebController { model.addAttribute("currentPage", "view-pdf"); return "view-pdf"; } - + @GetMapping("/multi-tool") @Hidden public String multiToolForm(Model model) { model.addAttribute("currentPage", "multi-tool"); return "multi-tool"; } - - + @GetMapping("/remove-pages") @Hidden public String pageDeleter(Model model) { model.addAttribute("currentPage", "remove-pages"); return "remove-pages"; } - + @GetMapping("/pdf-organizer") @Hidden public String pageOrganizer(Model model) { model.addAttribute("currentPage", "pdf-organizer"); return "pdf-organizer"; } - + @GetMapping("/extract-page") @Hidden public String extractPages(Model model) { model.addAttribute("currentPage", "extract-page"); return "extract-page"; } - + @GetMapping("/pdf-to-single-page") @Hidden public String pdfToSinglePage(Model model) { model.addAttribute("currentPage", "pdf-to-single-page"); return "pdf-to-single-page"; } - + @GetMapping("/rotate-pdf") @Hidden public String rotatePdfForm(Model model) { model.addAttribute("currentPage", "rotate-pdf"); return "rotate-pdf"; } - + @GetMapping("/split-pdfs") @Hidden public String splitPdfForm(Model model) { model.addAttribute("currentPage", "split-pdfs"); return "split-pdfs"; } - + @GetMapping("/sign") @Hidden public String signForm(Model model) { @@ -162,22 +168,19 @@ public class GeneralWebController { return "add-elements"; } - @GetMapping("/multi-page-layout") @Hidden public String multiPageLayoutForm(Model model) { model.addAttribute("currentPage", "multi-page-layout"); return "multi-page-layout"; } - - + @GetMapping("/scale-pages") @Hidden public String scalePagesFrom(Model model) { model.addAttribute("currentPage", "scale-pages"); return "scale-pages"; } - @GetMapping("/split-by-size-or-count") @Hidden @@ -185,19 +188,16 @@ public class GeneralWebController { model.addAttribute("currentPage", "split-by-size-or-count"); return "split-by-size-or-count"; } - + @GetMapping("/overlay-pdf") @Hidden public String overlayPdf(Model model) { model.addAttribute("currentPage", "overlay-pdf"); return "overlay-pdf"; } - - - @Autowired - private ResourceLoader resourceLoader; - + @Autowired private ResourceLoader resourceLoader; + private List getFontNames() { List fontNames = new ArrayList<>(); @@ -212,25 +212,27 @@ public class GeneralWebController { private List getFontNamesFromLocation(String locationPattern) { try { - Resource[] resources = ResourcePatternUtils.getResourcePatternResolver(resourceLoader) - .getResources(locationPattern); + Resource[] resources = + ResourcePatternUtils.getResourcePatternResolver(resourceLoader) + .getResources(locationPattern); return Arrays.stream(resources) - .map(resource -> { - try { - String filename = resource.getFilename(); - if (filename != null) { - int lastDotIndex = filename.lastIndexOf('.'); - if (lastDotIndex != -1) { - String name = filename.substring(0, lastDotIndex); - String extension = filename.substring(lastDotIndex + 1); - return new FontResource(name, extension); + .map( + resource -> { + try { + String filename = resource.getFilename(); + if (filename != null) { + int lastDotIndex = filename.lastIndexOf('.'); + if (lastDotIndex != -1) { + String name = filename.substring(0, lastDotIndex); + String extension = filename.substring(lastDotIndex + 1); + return new FontResource(name, extension); + } + } + return null; + } catch (Exception e) { + throw new RuntimeException("Error processing filename", e); } - } - return null; - } catch (Exception e) { - throw new RuntimeException("Error processing filename", e); - } - }) + }) .filter(Objects::nonNull) .collect(Collectors.toList()); } catch (Exception e) { @@ -238,64 +240,65 @@ public class GeneralWebController { } } - public String getFormatFromExtension(String extension) { switch (extension) { - case "ttf": return "truetype"; - case "woff": return "woff"; - case "woff2": return "woff2"; - case "eot": return "embedded-opentype"; - case "svg": return "svg"; - default: return ""; // or throw an exception if an unexpected extension is encountered + case "ttf": + return "truetype"; + case "woff": + return "woff"; + case "woff2": + return "woff2"; + case "eot": + return "embedded-opentype"; + case "svg": + return "svg"; + default: + return ""; // or throw an exception if an unexpected extension is encountered } } - public class FontResource { private String name; private String extension; private String type; + public FontResource(String name, String extension) { this.name = name; this.extension = extension; this.type = getFormatFromExtension(extension); } - public String getName() { - return name; - } + public String getName() { + return name; + } - public void setName(String name) { - this.name = name; - } + public void setName(String name) { + this.name = name; + } - public String getExtension() { - return extension; - } + public String getExtension() { + return extension; + } - public void setExtension(String extension) { - this.extension = extension; - } + public void setExtension(String extension) { + this.extension = extension; + } - public String getType() { - return type; - } + public String getType() { + return type; + } - public void setType(String type) { - this.type = type; - } - - + public void setType(String type) { + this.type = type; + } } - @GetMapping("/crop") @Hidden public String cropForm(Model model) { model.addAttribute("currentPage", "crop"); return "crop"; } - @GetMapping("/auto-split-pdf") @Hidden diff --git a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java index b6d08fbf..23c19f9a 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/HomeWebController.java @@ -8,20 +8,19 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; import io.swagger.v3.oas.annotations.Hidden; + import stirling.software.SPDF.model.ApplicationProperties; @Controller public class HomeWebController { - + @GetMapping("/about") @Hidden public String gameForm(Model model) { model.addAttribute("currentPage", "about"); return "about"; } - - - + @GetMapping("/") public String home(Model model) { model.addAttribute("currentPage", "home"); @@ -32,21 +31,18 @@ public class HomeWebController { public String root(Model model) { return "redirect:/"; } - - @Autowired - ApplicationProperties applicationProperties; + @Autowired ApplicationProperties applicationProperties; @GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE) @ResponseBody @Hidden public String getRobotsTxt() { Boolean allowGoogle = applicationProperties.getSystem().getGooglevisibility(); - if(Boolean.TRUE.equals(allowGoogle)) { + if (Boolean.TRUE.equals(allowGoogle)) { return "User-agent: Googlebot\nAllow: /\n\nUser-agent: *\nAllow: /"; } else { return "User-agent: Googlebot\nDisallow: /\n\nUser-agent: *\nDisallow: /"; } } - } diff --git a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java index e87a9495..fcfa2787 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.controller.web; + import java.time.Duration; import java.time.LocalDateTime; import java.util.Comparator; @@ -22,6 +23,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; + import jakarta.annotation.PostConstruct; import stirling.software.SPDF.config.StartupApplicationListener; import stirling.software.SPDF.model.ApplicationProperties; @@ -31,30 +33,28 @@ import stirling.software.SPDF.model.ApplicationProperties; @Tag(name = "Info", description = "Info APIs") public class MetricsController { - - @Autowired - ApplicationProperties applicationProperties; - - + @Autowired ApplicationProperties applicationProperties; + private final MeterRegistry meterRegistry; private boolean metricsEnabled; - + @PostConstruct public void init() { - Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); - if(metricsEnabled == null) - metricsEnabled = true; + Boolean metricsEnabled = applicationProperties.getMetrics().getEnabled(); + if (metricsEnabled == null) metricsEnabled = true; this.metricsEnabled = metricsEnabled; } - + public MetricsController(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } @GetMapping("/status") - @Operation(summary = "Application status and version", - description = "This endpoint returns the status of the application and its version number.") + @Operation( + summary = "Application status and version", + description = + "This endpoint returns the status of the application and its version number.") public ResponseEntity getStatus() { if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); @@ -65,38 +65,46 @@ public class MetricsController { status.put("version", getClass().getPackage().getImplementationVersion()); return ResponseEntity.ok(status); } - + @GetMapping("/loads") - @Operation(summary = "GET request count", - description = "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") - public ResponseEntity getPageLoads(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - if (!metricsEnabled) { + @Operation( + summary = "GET request count", + description = + "This endpoint returns the total count of GET requests or the count of GET requests for a specific endpoint.") + public ResponseEntity getPageLoads( + @RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") + Optional endpoint) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } - try { + try { double count = 0.0; - + for (Meter meter : meterRegistry.getMeters()) { if (meter.getId().getName().equals("http.requests")) { String method = meter.getId().getTag("method"); if (method != null && method.equals("GET")) { - - if (endpoint.isPresent() && !endpoint.get().isBlank()) { - if(!endpoint.get().startsWith("/")) { - endpoint = Optional.of("/" + endpoint.get()); - } - System.out.println("loads " + endpoint.get() + " vs " + meter.getId().getTag("uri")); - if(endpoint.get().equals(meter.getId().getTag("uri"))){ - if (meter instanceof Counter) { - count += ((Counter) meter).count(); - } - } - } else { - if (meter instanceof Counter) { - count += ((Counter) meter).count(); - } - } + + if (endpoint.isPresent() && !endpoint.get().isBlank()) { + if (!endpoint.get().startsWith("/")) { + endpoint = Optional.of("/" + endpoint.get()); + } + System.out.println( + "loads " + + endpoint.get() + + " vs " + + meter.getId().getTag("uri")); + if (endpoint.get().equals(meter.getId().getTag("uri"))) { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } + } else { + if (meter instanceof Counter) { + count += ((Counter) meter).count(); + } + } } } } @@ -108,10 +116,11 @@ public class MetricsController { } @GetMapping("/loads/all") - @Operation(summary = "GET requests count for all endpoints", + @Operation( + summary = "GET requests count for all endpoints", description = "This endpoint returns the count of GET requests for each endpoint.") public ResponseEntity getAllEndpointLoads() { - if (!metricsEnabled) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } try { @@ -133,10 +142,11 @@ public class MetricsController { } } - List results = counts.entrySet().stream() - .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) - .collect(Collectors.toList()); + List results = + counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); return ResponseEntity.ok(results); } catch (Exception e) { @@ -147,35 +157,41 @@ public class MetricsController { public class EndpointCount { private String endpoint; private double count; - - public EndpointCount(String endpoint, double count) { - this.endpoint = endpoint; - this.count = count; - } - public String getEndpoint() { - return endpoint; - } - public void setEndpoint(String endpoint) { - this.endpoint = endpoint; - } - public double getCount() { - return count; - } - public void setCount(double count) { - this.count = count; - } + public EndpointCount(String endpoint, double count) { + this.endpoint = endpoint; + this.count = count; + } + + public String getEndpoint() { + return endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public double getCount() { + return count; + } + + public void setCount(double count) { + this.count = count; + } } - @GetMapping("/requests") - @Operation(summary = "POST request count", - description = "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") - public ResponseEntity getTotalRequests(@RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") Optional endpoint) { - if (!metricsEnabled) { + @Operation( + summary = "POST request count", + description = + "This endpoint returns the total count of POST requests or the count of POST requests for a specific endpoint.") + public ResponseEntity getTotalRequests( + @RequestParam(required = false, name = "endpoint") @Parameter(description = "endpoint") + Optional endpoint) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } - try { + try { double count = 0.0; for (Meter meter : meterRegistry.getMeters()) { @@ -200,18 +216,18 @@ public class MetricsController { } } } - return ResponseEntity.ok(count); + return ResponseEntity.ok(count); } catch (Exception e) { - return ResponseEntity.ok(-1); + return ResponseEntity.ok(-1); } } - @GetMapping("/requests/all") - @Operation(summary = "POST requests count for all endpoints", + @Operation( + summary = "POST requests count for all endpoints", description = "This endpoint returns the count of POST requests for each endpoint.") public ResponseEntity getAllPostRequests() { - if (!metricsEnabled) { + if (!metricsEnabled) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("This endpoint is disabled."); } try { @@ -238,10 +254,11 @@ public class MetricsController { } } - List results = counts.entrySet().stream() - .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) - .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) - .collect(Collectors.toList()); + List results = + counts.entrySet().stream() + .map(entry -> new EndpointCount(entry.getKey(), entry.getValue())) + .sorted(Comparator.comparing(EndpointCount::getCount).reversed()) + .collect(Collectors.toList()); return ResponseEntity.ok(results); } catch (Exception e) { @@ -249,7 +266,6 @@ public class MetricsController { } } - @GetMapping("/uptime") public ResponseEntity getUptime() { if (!metricsEnabled) { diff --git a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java index ce4ae649..b0204779 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/OtherWebController.java @@ -23,7 +23,7 @@ public class OtherWebController { model.addAttribute("currentPage", "compress-pdf"); return "misc/compress-pdf"; } - + @GetMapping("/extract-image-scans") @Hidden public ModelAndView extractImageScansForm() { @@ -31,37 +31,34 @@ public class OtherWebController { modelAndView.addObject("currentPage", "extract-image-scans"); return modelAndView; } - + @GetMapping("/show-javascript") @Hidden public String extractJavascriptForm(Model model) { model.addAttribute("currentPage", "show-javascript"); return "misc/show-javascript"; } - - + @GetMapping("/add-page-numbers") @Hidden public String addPageNumbersForm(Model model) { model.addAttribute("currentPage", "add-page-numbers"); return "misc/add-page-numbers"; } - + @GetMapping("/extract-images") @Hidden public String extractImagesForm(Model model) { model.addAttribute("currentPage", "extract-images"); return "misc/extract-images"; } - + @GetMapping("/flatten") @Hidden public String flattenForm(Model model) { model.addAttribute("currentPage", "flatten"); return "misc/flatten"; } - - @GetMapping("/change-metadata") @Hidden @@ -69,22 +66,25 @@ public class OtherWebController { model.addAttribute("currentPage", "change-metadata"); return "misc/change-metadata"; } - + @GetMapping("/compare") @Hidden public String compareForm(Model model) { model.addAttribute("currentPage", "compare"); return "misc/compare"; } - + public List getAvailableTesseractLanguages() { String tessdataDir = "/usr/share/tesseract-ocr/5/tessdata"; File[] files = new File(tessdataDir).listFiles(); if (files == null) { return Collections.emptyList(); } - return Arrays.stream(files).filter(file -> file.getName().endsWith(".traineddata")).map(file -> file.getName().replace(".traineddata", "")) - .filter(lang -> !lang.equalsIgnoreCase("osd")).collect(Collectors.toList()); + return Arrays.stream(files) + .filter(file -> file.getName().endsWith(".traineddata")) + .map(file -> file.getName().replace(".traineddata", "")) + .filter(lang -> !lang.equalsIgnoreCase("osd")) + .collect(Collectors.toList()); } @GetMapping("/ocr-pdf") @@ -97,29 +97,28 @@ public class OtherWebController { modelAndView.addObject("currentPage", "ocr-pdf"); return modelAndView; } - - + @GetMapping("/add-image") @Hidden public String overlayImage(Model model) { model.addAttribute("currentPage", "add-image"); return "misc/add-image"; } - + @GetMapping("/adjust-contrast") @Hidden public String contrast(Model model) { model.addAttribute("currentPage", "adjust-contrast"); return "misc/adjust-contrast"; } - + @GetMapping("/repair") @Hidden public String repairForm(Model model) { model.addAttribute("currentPage", "repair"); return "misc/repair"; } - + @GetMapping("/remove-blanks") @Hidden public String removeBlanksForm(Model model) { @@ -140,14 +139,11 @@ public class OtherWebController { model.addAttribute("currentPage", "auto-crop"); return "misc/auto-crop"; } - + @GetMapping("/auto-rename") @Hidden public String autoRenameForm(Model model) { model.addAttribute("currentPage", "auto-rename"); return "misc/auto-rename"; } - - - } diff --git a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java index 2cbf245f..ba67e1d7 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/SecurityWebController.java @@ -10,20 +10,21 @@ import io.swagger.v3.oas.annotations.tags.Tag; @Controller @Tag(name = "Security", description = "Security APIs") public class SecurityWebController { - - @GetMapping("/auto-redact") + + @GetMapping("/auto-redact") @Hidden public String autoRedactForm(Model model) { model.addAttribute("currentPage", "auto-redact"); return "security/auto-redact"; } - + @GetMapping("/add-password") @Hidden public String addPasswordForm(Model model) { model.addAttribute("currentPage", "add-password"); return "security/add-password"; } + @GetMapping("/change-permissions") @Hidden public String permissionsForm(Model model) { @@ -44,21 +45,21 @@ public class SecurityWebController { model.addAttribute("currentPage", "add-watermark"); return "security/add-watermark"; } - + @GetMapping("/cert-sign") @Hidden public String certSignForm(Model model) { model.addAttribute("currentPage", "cert-sign"); return "security/cert-sign"; } - + @GetMapping("/sanitize-pdf") @Hidden public String sanitizeForm(Model model) { model.addAttribute("currentPage", "sanitize-pdf"); return "security/sanitize-pdf"; } - + @GetMapping("/get-info-on-pdf") @Hidden public String getInfo(Model model) { diff --git a/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java new file mode 100644 index 00000000..0d707e3b --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/ApiEndpoint.java @@ -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 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 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 + "]"; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java index 54b61c1b..f19fa1e9 100644 --- a/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java +++ b/src/main/java/stirling/software/SPDF/model/ApiKeyAuthenticationToken.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + import java.util.Collection; import org.springframework.security.authentication.AbstractAuthenticationToken; @@ -16,9 +17,10 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { setAuthenticated(false); } - public ApiKeyAuthenticationToken(Object principal, String apiKey, Collection authorities) { + public ApiKeyAuthenticationToken( + Object principal, String apiKey, Collection authorities) { super(authorities); - this.principal = principal; // principal can be a UserDetails object + this.principal = principal; // principal can be a UserDetails object this.credentials = apiKey; super.setAuthenticated(true); // this authentication is trusted } @@ -36,7 +38,8 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { - throw new IllegalArgumentException("Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); + throw new IllegalArgumentException( + "Cannot set this token to trusted. Use constructor which takes a GrantedAuthority list instead."); } super.setAuthenticated(false); } diff --git a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java index dc068779..a41d641c 100644 --- a/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java +++ b/src/main/java/stirling/software/SPDF/model/ApplicationProperties.java @@ -12,324 +12,376 @@ import stirling.software.SPDF.config.YamlPropertySourceFactory; @ConfigurationProperties(prefix = "") @PropertySource(value = "file:./configs/settings.yml", factory = YamlPropertySourceFactory.class) public class ApplicationProperties { - private Security security; - private System system; - private Ui ui; - private Endpoints endpoints; - private Metrics metrics; - private AutomaticallyGenerated automaticallyGenerated; - private AutoPipeline autoPipeline; - - public AutoPipeline getAutoPipeline() { - return autoPipeline != null ? autoPipeline : new AutoPipeline(); - } - - public void setAutoPipeline(AutoPipeline autoPipeline) { - this.autoPipeline = autoPipeline; - } - - public Security getSecurity() { - return security != null ? security : new Security(); - } - - public void setSecurity(Security security) { - this.security = security; - } - - public System getSystem() { - return system != null ? system : new System(); - } - - public void setSystem(System system) { - this.system = system; - } - - public Ui getUi() { - return ui != null ? ui : new Ui(); - } - - public void setUi(Ui ui) { - this.ui = ui; - } - - public Endpoints getEndpoints() { - return endpoints != null ? endpoints : new Endpoints(); - } - - public void setEndpoints(Endpoints endpoints) { - this.endpoints = endpoints; - } - - public Metrics getMetrics() { - return metrics != null ? metrics : new Metrics(); - } - - public void setMetrics(Metrics metrics) { - this.metrics = metrics; - } - - public AutomaticallyGenerated getAutomaticallyGenerated() { - return automaticallyGenerated != null ? automaticallyGenerated : new AutomaticallyGenerated(); - } - - public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { - this.automaticallyGenerated = automaticallyGenerated; - } - - @Override - public String toString() { - return "ApplicationProperties [security=" + security + ", system=" + system + ", ui=" + ui + ", endpoints=" - + endpoints + ", metrics=" + metrics + ", automaticallyGenerated=" + automaticallyGenerated - + ", autoPipeline=" + autoPipeline + "]"; - } - - public static class AutoPipeline { - private String outputFolder; - - public String getOutputFolder() { - return outputFolder; - } - - public void setOutputFolder(String outputFolder) { - this.outputFolder = outputFolder; - } - - @Override - public String toString() { - return "AutoPipeline [outputFolder=" + outputFolder + "]"; - } - - - - } - public static class Security { - private Boolean enableLogin; - private Boolean csrfDisabled; - private InitialLogin initialLogin; - - public InitialLogin getInitialLogin() { - return initialLogin != null ? initialLogin : new InitialLogin(); - } - - public void setInitialLogin(InitialLogin initialLogin) { - this.initialLogin = initialLogin; - } - - public Boolean getEnableLogin() { - return enableLogin; - } - - public void setEnableLogin(Boolean enableLogin) { - this.enableLogin = enableLogin; - } - - public Boolean getCsrfDisabled() { - return csrfDisabled; - } - - public void setCsrfDisabled(Boolean csrfDisabled) { - this.csrfDisabled = csrfDisabled; - } - - - @Override - public String toString() { - return "Security [enableLogin=" + enableLogin + ", initialLogin=" + initialLogin + ", csrfDisabled=" - + csrfDisabled + "]"; - } - - public static class InitialLogin { - - private String username; - private String password; - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - @Override - public String toString() { - return "InitialLogin [username=" + username + ", password=" + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - - - } - } - - public static class System { - private String defaultLocale; - private Boolean googlevisibility; - private String rootURIPath; - private String customStaticFilePath; - private Integer maxFileSize; - - public String getDefaultLocale() { - return defaultLocale; - } - - public void setDefaultLocale(String defaultLocale) { - this.defaultLocale = defaultLocale; - } - - public Boolean getGooglevisibility() { - return googlevisibility; - } - - public void setGooglevisibility(Boolean googlevisibility) { - this.googlevisibility = googlevisibility; - } - - public String getRootURIPath() { - return rootURIPath; - } - - public void setRootURIPath(String rootURIPath) { - this.rootURIPath = rootURIPath; - } - - public String getCustomStaticFilePath() { - return customStaticFilePath; - } - - public void setCustomStaticFilePath(String customStaticFilePath) { - this.customStaticFilePath = customStaticFilePath; - } - - public Integer getMaxFileSize() { - return maxFileSize; - } - - public void setMaxFileSize(Integer maxFileSize) { - this.maxFileSize = maxFileSize; - } - - @Override - public String toString() { - return "System [defaultLocale=" + defaultLocale + ", googlevisibility=" + googlevisibility + ", rootURIPath=" - + rootURIPath + ", customStaticFilePath=" + customStaticFilePath + ", maxFileSize=" + maxFileSize - + "]"; - } - - - } - - public static class Ui { - private String appName; - private String homeDescription; - private String appNameNavbar; - - public String getAppName() { - if(appName != null && appName.trim().length() == 0) - return null; - return appName; - } - - public void setAppName(String appName) { - this.appName = appName; - } - - public String getHomeDescription() { - if(homeDescription != null && homeDescription.trim().length() == 0) - return null; - return homeDescription; - } - - public void setHomeDescription(String homeDescription) { - this.homeDescription = homeDescription; - } - - public String getAppNameNavbar() { - if(appNameNavbar != null && appNameNavbar.trim().length() == 0) - return null; - return appNameNavbar; - } - - public void setAppNameNavbar(String appNameNavbar) { - this.appNameNavbar = appNameNavbar; - } - - @Override - public String toString() { - return "UserInterface [appName=" + appName + ", homeDescription=" + homeDescription + ", appNameNavbar=" + appNameNavbar + "]"; - } - } - - - public static class Endpoints { - private List toRemove; - private List groupsToRemove; - - public List getToRemove() { - return toRemove; - } - - public void setToRemove(List toRemove) { - this.toRemove = toRemove; - } - - public List getGroupsToRemove() { - return groupsToRemove; - } - - public void setGroupsToRemove(List groupsToRemove) { - this.groupsToRemove = groupsToRemove; - } - - @Override - public String toString() { - return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; - } - - - } - - public static class Metrics { - private Boolean enabled; - - public Boolean getEnabled() { - return enabled; - } - - public void setEnabled(Boolean enabled) { - this.enabled = enabled; - } - - @Override - public String toString() { - return "Metrics [enabled=" + enabled + "]"; - } - - - } - - public static class AutomaticallyGenerated { - private String key; - - public String getKey() { - return key; - } - - public void setKey(String key) { - this.key = key; - } - - @Override - public String toString() { - return "AutomaticallyGenerated [key=" + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + "]"; - } - - } + private Security security; + private System system; + private Ui ui; + private Endpoints endpoints; + private Metrics metrics; + private AutomaticallyGenerated automaticallyGenerated; + private AutoPipeline autoPipeline; + + public AutoPipeline getAutoPipeline() { + return autoPipeline != null ? autoPipeline : new AutoPipeline(); + } + + public void setAutoPipeline(AutoPipeline autoPipeline) { + this.autoPipeline = autoPipeline; + } + + public Security getSecurity() { + return security != null ? security : new Security(); + } + + public void setSecurity(Security security) { + this.security = security; + } + + public System getSystem() { + return system != null ? system : new System(); + } + + public void setSystem(System system) { + this.system = system; + } + + public Ui getUi() { + return ui != null ? ui : new Ui(); + } + + public void setUi(Ui ui) { + this.ui = ui; + } + + public Endpoints getEndpoints() { + return endpoints != null ? endpoints : new Endpoints(); + } + + public void setEndpoints(Endpoints endpoints) { + this.endpoints = endpoints; + } + + public Metrics getMetrics() { + return metrics != null ? metrics : new Metrics(); + } + + public void setMetrics(Metrics metrics) { + this.metrics = metrics; + } + + public AutomaticallyGenerated getAutomaticallyGenerated() { + return automaticallyGenerated != null + ? automaticallyGenerated + : new AutomaticallyGenerated(); + } + + public void setAutomaticallyGenerated(AutomaticallyGenerated automaticallyGenerated) { + this.automaticallyGenerated = automaticallyGenerated; + } + + @Override + public String toString() { + return "ApplicationProperties [security=" + + security + + ", system=" + + system + + ", ui=" + + ui + + ", endpoints=" + + endpoints + + ", metrics=" + + metrics + + ", automaticallyGenerated=" + + automaticallyGenerated + + ", autoPipeline=" + + autoPipeline + + "]"; + } + + public static class AutoPipeline { + private String outputFolder; + + public String getOutputFolder() { + return outputFolder; + } + + public void setOutputFolder(String outputFolder) { + this.outputFolder = outputFolder; + } + + @Override + public String toString() { + return "AutoPipeline [outputFolder=" + outputFolder + "]"; + } + } + + public static class Security { + private Boolean enableLogin; + private Boolean csrfDisabled; + 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() { + return initialLogin != null ? initialLogin : new InitialLogin(); + } + + public void setInitialLogin(InitialLogin initialLogin) { + this.initialLogin = initialLogin; + } + + public Boolean getEnableLogin() { + return enableLogin; + } + + public void setEnableLogin(Boolean enableLogin) { + this.enableLogin = enableLogin; + } + + public Boolean getCsrfDisabled() { + return csrfDisabled; + } + + public void setCsrfDisabled(Boolean csrfDisabled) { + this.csrfDisabled = csrfDisabled; + } + + @Override + public String toString() { + return "Security [enableLogin=" + + enableLogin + + ", initialLogin=" + + initialLogin + + ", csrfDisabled=" + + csrfDisabled + + "]"; + } + + public static class InitialLogin { + + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "InitialLogin [username=" + + username + + ", password=" + + (password != null && !password.isEmpty() ? "MASKED" : "NULL") + + "]"; + } + } + } + + public static class System { + private String defaultLocale; + private Boolean googlevisibility; + private String rootURIPath; + private String customStaticFilePath; + private Integer maxFileSize; + + private Boolean enableAlphaFunctionality; + + public Boolean getEnableAlphaFunctionality() { + return enableAlphaFunctionality; + } + + public void setEnableAlphaFunctionality(Boolean enableAlphaFunctionality) { + this.enableAlphaFunctionality = enableAlphaFunctionality; + } + + public String getDefaultLocale() { + return defaultLocale; + } + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public Boolean getGooglevisibility() { + return googlevisibility; + } + + public void setGooglevisibility(Boolean googlevisibility) { + this.googlevisibility = googlevisibility; + } + + public String getRootURIPath() { + return rootURIPath; + } + + public void setRootURIPath(String rootURIPath) { + this.rootURIPath = rootURIPath; + } + + public String getCustomStaticFilePath() { + return customStaticFilePath; + } + + public void setCustomStaticFilePath(String customStaticFilePath) { + this.customStaticFilePath = customStaticFilePath; + } + + public Integer getMaxFileSize() { + return maxFileSize; + } + + public void setMaxFileSize(Integer maxFileSize) { + this.maxFileSize = maxFileSize; + } + + @Override + public String toString() { + return "System [defaultLocale=" + + defaultLocale + + ", googlevisibility=" + + googlevisibility + + ", rootURIPath=" + + rootURIPath + + ", customStaticFilePath=" + + customStaticFilePath + + ", maxFileSize=" + + maxFileSize + + ", enableAlphaFunctionality=" + + enableAlphaFunctionality + + "]"; + } + } + + public static class Ui { + private String appName; + private String homeDescription; + private String appNameNavbar; + + public String getAppName() { + if (appName != null && appName.trim().length() == 0) return null; + return appName; + } + + public void setAppName(String appName) { + this.appName = appName; + } + + public String getHomeDescription() { + if (homeDescription != null && homeDescription.trim().length() == 0) return null; + return homeDescription; + } + + public void setHomeDescription(String homeDescription) { + this.homeDescription = homeDescription; + } + + public String getAppNameNavbar() { + if (appNameNavbar != null && appNameNavbar.trim().length() == 0) return null; + return appNameNavbar; + } + + public void setAppNameNavbar(String appNameNavbar) { + this.appNameNavbar = appNameNavbar; + } + + @Override + public String toString() { + return "UserInterface [appName=" + + appName + + ", homeDescription=" + + homeDescription + + ", appNameNavbar=" + + appNameNavbar + + "]"; + } + } + + public static class Endpoints { + private List toRemove; + private List groupsToRemove; + + public List getToRemove() { + return toRemove; + } + + public void setToRemove(List toRemove) { + this.toRemove = toRemove; + } + + public List getGroupsToRemove() { + return groupsToRemove; + } + + public void setGroupsToRemove(List groupsToRemove) { + this.groupsToRemove = groupsToRemove; + } + + @Override + public String toString() { + return "Endpoints [toRemove=" + toRemove + ", groupsToRemove=" + groupsToRemove + "]"; + } + } + + public static class Metrics { + private Boolean enabled; + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + @Override + public String toString() { + return "Metrics [enabled=" + enabled + "]"; + } + } + + public static class AutomaticallyGenerated { + private String key; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + @Override + public String toString() { + return "AutomaticallyGenerated [key=" + + (key != null && !key.isEmpty() ? "MASKED" : "NULL") + + "]"; + } + } } diff --git a/src/main/java/stirling/software/SPDF/model/AttemptCounter.java b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java new file mode 100644 index 00000000..7cb13ee0 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/AttemptCounter.java @@ -0,0 +1,28 @@ +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; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Authority.java b/src/main/java/stirling/software/SPDF/model/Authority.java index 8be853ea..57ba538e 100644 --- a/src/main/java/stirling/software/SPDF/model/Authority.java +++ b/src/main/java/stirling/software/SPDF/model/Authority.java @@ -13,19 +13,15 @@ import jakarta.persistence.Table; @Table(name = "authorities") public class Authority { - public Authority() { + public Authority() {} - } - - - public Authority(String authority, User user) { - this.authority = authority; - this.user = user; - user.getAuthorities().add(this); - } + public Authority(String authority, User user) { + this.authority = authority; + this.user = user; + user.getAuthorities().add(this); + } - - @Id + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -36,29 +32,27 @@ public class Authority { @JoinColumn(name = "user_id") private User user; - public Long getId() { - return id; - } + public Long getId() { + return id; + } - public void setId(Long id) { - this.id = id; - } + public void setId(Long id) { + this.id = id; + } - public String getAuthority() { - return authority; - } + public String getAuthority() { + return authority; + } - public void setAuthority(String authority) { - this.authority = authority; - } + public void setAuthority(String authority) { + this.authority = authority; + } - public User getUser() { - return user; - } + public User getUser() { + return user; + } - public void setUser(User user) { - this.user = user; - } - - + public void setUser(User user) { + this.user = user; + } } diff --git a/src/main/java/stirling/software/SPDF/model/PDFText.java b/src/main/java/stirling/software/SPDF/model/PDFText.java index 9a4909d0..9c460f3c 100644 --- a/src/main/java/stirling/software/SPDF/model/PDFText.java +++ b/src/main/java/stirling/software/SPDF/model/PDFText.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + public class PDFText { private final int pageIndex; private final float x1; @@ -39,4 +40,4 @@ public class PDFText { public String getText() { return text; } -} \ No newline at end of file +} diff --git a/src/main/java/stirling/software/SPDF/model/PersistentLogin.java b/src/main/java/stirling/software/SPDF/model/PersistentLogin.java index 0747c1eb..cc94eea2 100644 --- a/src/main/java/stirling/software/SPDF/model/PersistentLogin.java +++ b/src/main/java/stirling/software/SPDF/model/PersistentLogin.java @@ -24,38 +24,37 @@ public class PersistentLogin { @Column(name = "last_used", nullable = false) private Date lastUsed; - public String getSeries() { - return series; - } + public String getSeries() { + return series; + } - public void setSeries(String series) { - this.series = series; - } + public void setSeries(String series) { + this.series = series; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public void setUsername(String username) { - this.username = username; - } + public void setUsername(String username) { + this.username = username; + } - public String getToken() { - return token; - } + public String getToken() { + return token; + } - public void setToken(String token) { - this.token = token; - } + public void setToken(String token) { + this.token = token; + } - public Date getLastUsed() { - return lastUsed; - } + public Date getLastUsed() { + return lastUsed; + } - public void setLastUsed(Date lastUsed) { - this.lastUsed = lastUsed; - } + public void setLastUsed(Date lastUsed) { + this.lastUsed = lastUsed; + } - // Getters, setters, etc. } diff --git a/src/main/java/stirling/software/SPDF/model/PipelineConfig.java b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java index 77ef7a05..efb9b232 100644 --- a/src/main/java/stirling/software/SPDF/model/PipelineConfig.java +++ b/src/main/java/stirling/software/SPDF/model/PipelineConfig.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.model; + import java.util.List; import com.fasterxml.jackson.annotation.JsonProperty; @@ -14,7 +15,6 @@ public class PipelineConfig { @JsonProperty("outputFileName") private String outputPattern; - public String getName() { return name; } @@ -46,6 +46,4 @@ public class PipelineConfig { public void setOutputPattern(String outputPattern) { this.outputPattern = outputPattern; } - - } diff --git a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java index 10c27bfc..f6183505 100644 --- a/src/main/java/stirling/software/SPDF/model/PipelineOperation.java +++ b/src/main/java/stirling/software/SPDF/model/PipelineOperation.java @@ -3,30 +3,27 @@ package stirling.software.SPDF.model; import java.util.Map; public class PipelineOperation { - private String operation; - private Map parameters; + private String operation; + private Map parameters; + public String getOperation() { + return operation; + } - public String getOperation() { - return operation; - } + public void setOperation(String operation) { + this.operation = operation; + } - public void setOperation(String operation) { - this.operation = operation; - } + public Map getParameters() { + return parameters; + } - public Map getParameters() { - return parameters; - } + public void setParameters(Map parameters) { + this.parameters = parameters; + } - public void setParameters(Map parameters) { - this.parameters = parameters; - } - - @Override - public String toString() { - return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; - } - - - } \ No newline at end of file + @Override + public String toString() { + return "PipelineOperation [operation=" + operation + ", parameters=" + parameters + "]"; + } +} diff --git a/src/main/java/stirling/software/SPDF/model/Role.java b/src/main/java/stirling/software/SPDF/model/Role.java index 1b775de0..5100e9dd 100644 --- a/src/main/java/stirling/software/SPDF/model/Role.java +++ b/src/main/java/stirling/software/SPDF/model/Role.java @@ -1,7 +1,8 @@ package stirling.software.SPDF.model; + public enum Role { - - // Unlimited access + + // Unlimited access ADMIN("ROLE_ADMIN", Integer.MAX_VALUE, Integer.MAX_VALUE), // Unlimited access @@ -14,7 +15,11 @@ public enum Role { EXTRA_LIMITED_API_USER("ROLE_EXTRA_LIMITED_API_USER", 20, 20), // 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 int apiCallsPerDay; @@ -37,7 +42,7 @@ public enum Role { public int getWebCallsPerDay() { return webCallsPerDay; } - + public static Role fromString(String roleId) { for (Role role : Role.values()) { if (role.getRoleId().equalsIgnoreCase(roleId)) { @@ -46,5 +51,4 @@ public enum Role { } throw new IllegalArgumentException("No Role defined for id: " + roleId); } - } diff --git a/src/main/java/stirling/software/SPDF/model/SortTypes.java b/src/main/java/stirling/software/SPDF/model/SortTypes.java index 21181cfa..eb10ff58 100644 --- a/src/main/java/stirling/software/SPDF/model/SortTypes.java +++ b/src/main/java/stirling/software/SPDF/model/SortTypes.java @@ -1,4 +1,12 @@ package stirling.software.SPDF.model; + public enum SortTypes { - REVERSE_ORDER, DUPLEX_SORT, BOOKLET_SORT, SIDE_STITCH_BOOKLET_SORT, ODD_EVEN_SPLIT, REMOVE_FIRST, REMOVE_LAST, REMOVE_FIRST_AND_LAST, -} \ No newline at end of file + REVERSE_ORDER, + DUPLEX_SORT, + BOOKLET_SORT, + SIDE_STITCH_BOOKLET_SORT, + ODD_EVEN_SPLIT, + REMOVE_FIRST, + REMOVE_LAST, + REMOVE_FIRST_AND_LAST, +} diff --git a/src/main/java/stirling/software/SPDF/model/User.java b/src/main/java/stirling/software/SPDF/model/User.java index f771a821..253b33da 100644 --- a/src/main/java/stirling/software/SPDF/model/User.java +++ b/src/main/java/stirling/software/SPDF/model/User.java @@ -19,15 +19,16 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.MapKeyColumn; import jakarta.persistence.OneToMany; import jakarta.persistence.Table; + @Entity @Table(name = "users") public class User { - @Id + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "user_id") private Long id; - + @Column(name = "username", unique = true) private String username; @@ -36,13 +37,13 @@ public class User { @Column(name = "apiKey") private String apiKey; - + @Column(name = "enabled") private boolean enabled; @Column(name = "isFirstLogin") private Boolean isFirstLogin = false; - + @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") private Set authorities = new HashSet<>(); @@ -50,85 +51,83 @@ public class User { @MapKeyColumn(name = "setting_key") @Column(name = "setting_value") @CollectionTable(name = "user_settings", joinColumns = @JoinColumn(name = "user_id")) - private Map settings = new HashMap<>(); // Key-value pairs of settings. + private Map settings = new HashMap<>(); // Key-value pairs of settings. - - public boolean isFirstLogin() { - return isFirstLogin != null && isFirstLogin; - } + public boolean isFirstLogin() { + return isFirstLogin != null && isFirstLogin; + } - public void setFirstLogin(boolean isFirstLogin) { - this.isFirstLogin = isFirstLogin; - } + public void setFirstLogin(boolean isFirstLogin) { + this.isFirstLogin = isFirstLogin; + } - public Long getId() { - return id; - } + public Long getId() { + return id; + } - public void setId(Long id) { - this.id = id; - } + public void setId(Long id) { + this.id = id; + } - public String getApiKey() { - return apiKey; - } + public String getApiKey() { + return apiKey; + } - public void setApiKey(String apiKey) { - this.apiKey = apiKey; - } + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } - public Map getSettings() { - return settings; - } + public Map getSettings() { + return settings; + } - public void setSettings(Map settings) { - this.settings = settings; - } + public void setSettings(Map settings) { + this.settings = settings; + } - public String getUsername() { - return username; - } + public String getUsername() { + return username; + } - public void setUsername(String username) { - this.username = username; - } + public void setUsername(String username) { + this.username = username; + } - public String getPassword() { - return password; - } + public String getPassword() { + return password; + } - public void setPassword(String password) { - this.password = password; - } + public void setPassword(String password) { + this.password = password; + } - public boolean isEnabled() { - return enabled; - } + public boolean isEnabled() { + return enabled; + } - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } - public Set getAuthorities() { - return authorities; - } + public Set getAuthorities() { + return authorities; + } - public void setAuthorities(Set authorities) { - this.authorities = authorities; - } - - public void addAuthorities(Set authorities) { - this.authorities.addAll(authorities); - } - public void addAuthority(Authority authorities) { - this.authorities.add(authorities); - } - - public String getRolesAsString() { - return this.authorities.stream() - .map(Authority::getAuthority) - .collect(Collectors.joining(", ")); - } + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + public void addAuthorities(Set authorities) { + this.authorities.addAll(authorities); + } + public void addAuthority(Authority authorities) { + this.authorities.add(authorities); + } + + public String getRolesAsString() { + return this.authorities.stream() + .map(Authority::getAuthority) + .collect(Collectors.joining(", ")); + } } diff --git a/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java b/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java index 441d904a..1c0581cd 100644 --- a/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/GeneralFile.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -12,6 +13,6 @@ import lombok.NoArgsConstructor; @NoArgsConstructor public class GeneralFile { - @Schema(description = "The input file") - private MultipartFile fileInput; + @Schema(description = "The input file") + private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java index 1d7a8afe..d4a4a6bd 100644 --- a/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/HandleDataRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -13,8 +14,8 @@ import lombok.NoArgsConstructor; public class HandleDataRequest { @Schema(description = "The input files") - private MultipartFile[] fileInputs; + private MultipartFile[] fileInput; @Schema(description = "JSON String") - private String jsonString; + private String json; } diff --git a/src/main/java/stirling/software/SPDF/model/api/ImageFile.java b/src/main/java/stirling/software/SPDF/model/api/ImageFile.java index 02079843..fdc0e6dc 100644 --- a/src/main/java/stirling/software/SPDF/model/api/ImageFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/ImageFile.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -11,6 +12,6 @@ import lombok.NoArgsConstructor; @NoArgsConstructor @EqualsAndHashCode public class ImageFile { - @Schema(description = "The input image file") + @Schema(description = "The input image file") private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java b/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java index 937a4265..00a34b74 100644 --- a/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java +++ b/src/main/java/stirling/software/SPDF/model/api/MultiplePDFFiles.java @@ -3,13 +3,15 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor @EqualsAndHashCode public class MultiplePDFFiles { - @Schema(description = "The input PDF files", type = "array", format = "binary") + @Schema(description = "The input PDF files", type = "array", format = "binary") private MultipartFile[] fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java b/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java index 1f902d88..47377188 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFComparison.java @@ -1,16 +1,18 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFComparison extends PDFFile { - - @Schema(description = "The comparison type, accepts Greater, Equal, Less than", allowableValues = { - "Greater", "Equal", "Less" }) + + @Schema( + description = "The comparison type, accepts Greater, Equal, Less than", + allowableValues = {"Greater", "Equal", "Less"}) private String comparator; - } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java b/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java index 14462f0a..04042bd8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFComparisonAndCount.java @@ -1,15 +1,15 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFComparisonAndCount extends PDFComparison { - @Schema(description = "Count") + @Schema(description = "Count") private String pageCount; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFFile.java b/src/main/java/stirling/software/SPDF/model/api/PDFFile.java index 378b3c03..68562190 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFFile.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFFile.java @@ -3,11 +3,13 @@ package stirling.software.SPDF.model.api; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; + @Data @EqualsAndHashCode public class PDFFile { - @Schema(description = "The input PDF file") + @Schema(description = "The input PDF file") private MultipartFile fileInput; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java index aa8fe08b..34f15c13 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithImageFormatRequest.java @@ -1,14 +1,16 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFWithImageFormatRequest extends PDFFile { - @Schema(description = "The output image format e.g., 'png', 'jpeg', or 'gif'", - allowableValues = { "png", "jpeg", "gif" }) + @Schema( + description = "The output image format e.g., 'png', 'jpeg', or 'gif'", + allowableValues = {"png", "jpeg", "gif"}) private String format; } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java index d53d8d12..aa664be0 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageNums.java @@ -5,35 +5,40 @@ import java.util.List; import org.apache.pdfbox.pdmodel.PDDocument; +import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import stirling.software.SPDF.utils.GeneralUtils; + @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) 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; - - - public List getPageNumbersList(){ - int pageCount = 0; - try { - pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return GeneralUtils.parsePageString(pageNumbers, pageCount); - - } - public List getPageNumbersList(PDDocument doc){ - int pageCount = 0; - pageCount = doc.getNumberOfPages(); - return GeneralUtils.parsePageString(pageNumbers, pageCount); - - } + + @Hidden + public List getPageNumbersList() { + int pageCount = 0; + try { + pageCount = PDDocument.load(getFileInput().getInputStream()).getNumberOfPages(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return GeneralUtils.parsePageString(pageNumbers, pageCount); + } + + @Hidden + public List getPageNumbersList(PDDocument doc) { + int pageCount = 0; + pageCount = doc.getNumberOfPages(); + return GeneralUtils.parsePageString(pageNumbers, pageCount); + } } diff --git a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java index 661a4ffe..139f492d 100644 --- a/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java +++ b/src/main/java/stirling/software/SPDF/model/api/PDFWithPageSize.java @@ -1,16 +1,17 @@ package stirling.software.SPDF.model.api; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFWithPageSize extends PDFFile { - @Schema(description = "The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL.", - allowableValues = { - "A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL" - }) - private String pageSize; + @Schema( + description = + "The scale of pages in the output PDF. Acceptable values are A0-A6, LETTER, LEGAL.", + allowableValues = {"A0", "A1", "A2", "A3", "A4", "A5", "A6", "LETTER", "LEGAL"}) + private String pageSize; } diff --git a/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java b/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java index 14112152..fcc5d5ba 100644 --- a/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/SplitPdfBySectionsRequest.java @@ -1,14 +1,16 @@ package stirling.software.SPDF.model.api; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @Data @NoArgsConstructor -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SplitPdfBySectionsRequest extends PDFFile { - @Schema(description = "Number of horizontal divisions for each PDF page", example = "2") + @Schema(description = "Number of horizontal divisions for each PDF page", example = "2") private int horizontalDivisions; @Schema(description = "Number of vertical divisions for each PDF page", example = "2") diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java index 18026618..eaa8e361 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToImageRequest.java @@ -1,21 +1,29 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ConvertToImageRequest extends PDFFile { - @Schema(description = "The output image format", allowableValues = {"png", "jpeg", "jpg", "gif"}) + @Schema( + description = "The output image format", + allowableValues = {"png", "jpeg", "jpg", "gif"}) private String imageFormat; - @Schema(description = "Choose between a single image containing all pages or separate images for each page", allowableValues = {"single", "multiple"}) + @Schema( + description = + "Choose between a single image containing all pages or separate images for each page", + allowableValues = {"single", "multiple"}) private String singleOrMultiple; - @Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"}) + @Schema( + description = "The color type of the output image(s)", + allowableValues = {"color", "greyscale", "blackwhite"}) private String colorType; @Schema(description = "The DPI (dots per inch) for the output image(s)") diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java index 37df6f9e..7630f746 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/ConvertToPdfRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.converters; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @@ -13,15 +14,18 @@ public class ConvertToPdfRequest { @Schema(description = "The input images to be converted to a PDF file") private MultipartFile[] fileInput; - @Schema(description = "Option to determine how the image will fit onto the page", - allowableValues = { "fillPage", "fitDocumentToImage", "maintainAspectRatio" }) + @Schema( + description = "Option to determine how the image will fit onto the page", + allowableValues = {"fillPage", "fitDocumentToImage", "maintainAspectRatio"}) private String fitOption; - - - @Schema(description = "The color type of the output image(s)", allowableValues = {"color", "greyscale", "blackwhite"}) + @Schema( + description = "The color type of the output image(s)", + allowableValues = {"color", "greyscale", "blackwhite"}) private String colorType; - @Schema(description = "Whether to automatically rotate the images to better fit the PDF page", example = "true") + @Schema( + description = "Whether to automatically rotate the images to better fit the PDF page", + example = "true") private boolean autoRotate; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java index 0e8b79ad..45064375 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToPresentationRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToPresentationRequest extends PDFFile { - @Schema(description = "The output Presentation format", allowableValues = {"ppt", "pptx", "odp"}) + @Schema( + description = "The output Presentation format", + allowableValues = {"ppt", "pptx", "odp"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java index 687ed621..5759d332 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToTextOrRTFRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToTextOrRTFRequest extends PDFFile { - @Schema(description = "The output Text or RTF format", allowableValues = {"rtf", "txt:Text"}) + @Schema( + description = "The output Text or RTF format", + allowableValues = {"rtf", "txt:Text"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java index 87150c73..db3c3dfb 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/PdfToWordRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PdfToWordRequest extends PDFFile { - @Schema(description = "The output Word document format", allowableValues = {"doc", "docx", "odt"}) + @Schema( + description = "The output Word document format", + allowableValues = {"doc", "docx", "odt"}) private String outputFormat; } diff --git a/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java index 4607c153..3e340868 100644 --- a/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/converters/UrlToPdfRequest.java @@ -1,6 +1,7 @@ package stirling.software.SPDF.model.api.converters; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; diff --git a/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java b/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java index bfe87a16..faf955c6 100644 --- a/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java +++ b/src/main/java/stirling/software/SPDF/model/api/extract/PDFFilePage.java @@ -1,18 +1,15 @@ package stirling.software.SPDF.model.api.extract; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFFilePage extends PDFFile { - @Schema(description = "Number of chosen page", type = "number") private int pageId; - - } - diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java index 0b6cb1cb..7cd2f76a 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/ContainsTextRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageNums; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ContainsTextRequest extends PDFWithPageNums { @Schema(description = "The text to check for", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java index ce9a9236..00d75139 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/FileSizeRequest.java @@ -1,16 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class FileSizeRequest extends PDFComparison { @Schema(description = "File Size", required = true) private String fileSize; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java index d5fb9739..949178c5 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/PageRotationRequest.java @@ -1,15 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PageRotationRequest extends PDFComparison { @Schema(description = "Rotation in degrees", required = true) private int rotation; - } diff --git a/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java b/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java index 12083636..c8b46a90 100644 --- a/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/filter/PageSizeRequest.java @@ -1,16 +1,15 @@ package stirling.software.SPDF.model.api.filter; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFComparison; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PageSizeRequest extends PDFComparison { @Schema(description = "Standard Page Size", required = true) private String standardPageSize; - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java b/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java index 52821515..cec72ce4 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/CropPdfForm.java @@ -1,18 +1,23 @@ package stirling.software.SPDF.model.api.general; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class CropPdfForm extends PDFFile { - - @Schema(description = "The x-coordinate of the top-left corner of the crop area", type = "number") + @Schema( + description = "The x-coordinate of the top-left corner of the crop area", + type = "number") private float x; - @Schema(description = "The y-coordinate of the top-left corner of the crop area", type = "number") + @Schema( + description = "The y-coordinate of the top-left corner of the crop area", + type = "number") private float y; @Schema(description = "The width of the crop area", type = "number") @@ -21,4 +26,3 @@ public class CropPdfForm extends PDFFile { @Schema(description = "The height of the crop area", type = "number") private float height; } - diff --git a/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java index 4642cb75..1ecdc2ee 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/MergeMultiplePagesRequest.java @@ -1,18 +1,21 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MergeMultiplePagesRequest extends PDFFile { - @Schema(description = "The number of pages to fit onto a single sheet in the output PDF.", - type = "integer", allowableValues = {"2", "3", "4", "9", "16"}) + @Schema( + description = "The number of pages to fit onto a single sheet in the output PDF.", + type = "integer", + allowableValues = {"2", "3", "4", "9", "16"}) private int pagesPerSheet; - + @Schema(description = "Boolean for if you wish to add border around the pages") private boolean addBorder; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java index b7b3bda7..0d0a98ae 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/MergePdfsRequest.java @@ -1,22 +1,24 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.MultiplePDFFiles; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MergePdfsRequest extends MultiplePDFFiles { - - @Schema(description = "The type of sorting to be applied on the input files before merging.", + + @Schema( + description = "The type of sorting to be applied on the input files before merging.", allowableValues = { - "orderProvided", - "byFileName", - "byDateModified", - "byDateCreated", + "orderProvided", + "byFileName", + "byDateModified", + "byDateCreated", "byPDFTitle" - }, + }, defaultValue = "orderProvided") private String sortType = "orderProvided"; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java index 13ca7857..458dd699 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/OverlayPdfsRequest.java @@ -1,24 +1,34 @@ package stirling.software.SPDF.model.api.general; + import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data @EqualsAndHashCode(callSuper = true) -public class OverlayPdfsRequest extends PDFFile { +public class OverlayPdfsRequest extends PDFFile { - @Schema(description = "An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.") + @Schema( + description = + "An array of PDF files to be used as overlays on the base PDF. The order in these files is applied based on the selected mode.") private MultipartFile[] overlayFiles; - @Schema(description = "The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", required = true) + @Schema( + description = + "The mode of overlaying: 'SequentialOverlay' for sequential application, 'InterleavedOverlay' for round-robin application, 'FixedRepeatOverlay' for fixed repetition based on provided counts", + required = true) private String overlayMode; - @Schema(description = "An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", required = false) + @Schema( + description = + "An array of integers specifying the number of times each corresponding overlay file should be applied in the 'FixedRepeatOverlay' mode. This should match the length of the overlayFiles array.", + required = false) private int[] counts; - + @Schema(description = "Overlay position 0 is Foregound, 1 is Background") private int overlayPosition; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java index 3e5b4f23..b20ced47 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/RearrangePagesRequest.java @@ -1,23 +1,26 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.SortTypes; import stirling.software.SPDF.model.api.PDFWithPageNums; + @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RearrangePagesRequest extends PDFWithPageNums { - @Schema(implementation = SortTypes.class, description = "The custom mode for page rearrangement. Valid values are:\n" - + "REVERSE_ORDER: Reverses the order of all pages.\n" - + "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " - + "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" - + "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" - + "REMOVE_FIRST: Removes the first page.\n" + "REMOVE_LAST: Removes the last page.\n" - + "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n") + @Schema( + implementation = SortTypes.class, + description = + "The custom mode for page rearrangement. Valid values are:\n" + + "REVERSE_ORDER: Reverses the order of all pages.\n" + + "DUPLEX_SORT: Sorts pages as if all fronts were scanned then all backs in reverse (1, n, 2, n-1, ...). " + + "BOOKLET_SORT: Arranges pages for booklet printing (last, first, second, second last, ...).\n" + + "ODD_EVEN_SPLIT: Splits and arranges pages into odd and even numbered pages.\n" + + "REMOVE_FIRST: Removes the first page.\n" + + "REMOVE_LAST: Removes the last page.\n" + + "REMOVE_FIRST_AND_LAST: Removes both the first and the last pages.\n") private String customMode; - - - } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java index 8f48c605..1efd7049 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/RotatePDFRequest.java @@ -1,14 +1,18 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RotatePDFRequest extends PDFFile { - @Schema(description = "The angle by which to rotate the PDF file. This should be a multiple of 90.", example = "90") + @Schema( + description = + "The angle by which to rotate the PDF file. This should be a multiple of 90.", + example = "90") private Integer angle; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java index ff44b01d..0ba004c9 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/ScalePagesRequest.java @@ -1,14 +1,17 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageSize; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ScalePagesRequest extends PDFWithPageSize { - @Schema(description = "The scale of the content on the pages of the output PDF. Acceptable values are floats.") + @Schema( + description = + "The scale of the content on the pages of the output PDF. Acceptable values are floats.") private float scaleFactor; } diff --git a/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java b/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java index 087ce80c..8b36c1c8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/general/SplitPdfBySizeOrCountRequest.java @@ -1,18 +1,26 @@ package stirling.software.SPDF.model.api.general; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SplitPdfBySizeOrCountRequest extends PDFFile { - @Schema(description = "Determines the type of split: 0 for size, 1 for page count, 2 for document count", required = false, defaultValue = "0") - private int splitType; + @Schema( + description = + "Determines the type of split: 0 for size, 1 for page count, 2 for document count", + required = false, + defaultValue = "0") + private int splitType; - - @Schema(description = "Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')", required = false, defaultValue = "10MB") + @Schema( + description = + "Value for split: size in MB (e.g., '10MB') or number of pages (e.g., '5')", + required = false, + defaultValue = "10MB") private String splitValue; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java index 313d42ab..5622903a 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/AddPageNumbersRequest.java @@ -1,15 +1,18 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFWithPageNums; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddPageNumbersRequest extends PDFWithPageNums { - @Schema(description = "Custom margin: small/medium/large", allowableValues = {"small", "medium", "large"}) + @Schema( + description = "Custom margin: small/medium/large", + allowableValues = {"small", "medium", "large"}) private String customMargin; @Schema(description = "Position: 1 of 9 positions", minimum = "1", maximum = "9") @@ -21,6 +24,8 @@ public class AddPageNumbersRequest extends PDFWithPageNums { @Schema(description = "Which pages to number, default all") private String pagesToNumber; - @Schema(description = "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"") + @Schema( + description = + "Custom text: defaults to just number but can have things like \"Page {n} of {p}\"") private String customText; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java index c4923746..c60d50e8 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/AutoSplitPdfRequest.java @@ -1,14 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AutoSplitPdfRequest extends PDFFile { - @Schema(description = "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", required = false, defaultValue = "false") + @Schema( + description = + "Flag indicating if the duplex mode is active, where the page after the divider also gets removed.", + required = false, + defaultValue = "false") private boolean duplexMode; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java index f3028445..33c941af 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractHeaderRequest.java @@ -1,14 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ExtractHeaderRequest extends PDFFile { - @Schema(description = "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", required = false, defaultValue = "false") + @Schema( + description = + "Flag indicating whether to use the first text as a fallback if no suitable title is found. Defaults to false.", + required = false, + defaultValue = "false") private boolean useFirstTextAsFallback; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java index 1a575fe6..6839451e 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ExtractImageScansRequest.java @@ -3,6 +3,7 @@ package stirling.software.SPDF.model.api.misc; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; @@ -12,18 +13,33 @@ public class ExtractImageScansRequest { @Schema(description = "The input file containing image scans", required = true) private MultipartFile fileInput; - @Schema(description = "The angle threshold for the image scan extraction", defaultValue = "5", example = "5") + @Schema( + description = "The angle threshold for the image scan extraction", + defaultValue = "5", + example = "5") private int angleThreshold = 5; - @Schema(description = "The tolerance for the image scan extraction", defaultValue = "20", example = "20") + @Schema( + description = "The tolerance for the image scan extraction", + defaultValue = "20", + example = "20") private int tolerance = 20; - @Schema(description = "The minimum area for the image scan extraction", defaultValue = "8000", example = "8000") + @Schema( + description = "The minimum area for the image scan extraction", + defaultValue = "8000", + example = "8000") private int minArea = 8000; - @Schema(description = "The minimum contour area for the image scan extraction", defaultValue = "500", example = "500") + @Schema( + description = "The minimum contour area for the image scan extraction", + defaultValue = "500", + example = "500") private int minContourArea = 500; - @Schema(description = "The border size for the image scan extraction", defaultValue = "1", example = "1") - private int borderSize =1; + @Schema( + description = "The border size for the image scan extraction", + defaultValue = "1", + example = "1") + private int borderSize = 1; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java index d62890aa..e638d7d2 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/MetadataRequest.java @@ -3,12 +3,13 @@ package stirling.software.SPDF.model.api.misc; import java.util.Map; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class MetadataRequest extends PDFFile { @Schema(description = "Delete all metadata if set to true") @@ -41,6 +42,8 @@ public class MetadataRequest extends PDFFile { @Schema(description = "The trapped status of the document") private String trapped; - @Schema(description = "Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard") + @Schema( + description = + "Map list of key and value of custom parameters. Note these must start with customKey and customValue if they are non-standard") private Map allRequestParams; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java index bc00cf20..96a787f3 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/OptimizePdfRequest.java @@ -1,16 +1,19 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class OptimizePdfRequest extends PDFFile { - @Schema(description = "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", - allowableValues = { "1", "2", "3", "4", "5" }) + @Schema( + description = + "The level of optimization to apply to the PDF file. Higher values indicate greater compression but may reduce quality.", + allowableValues = {"1", "2", "3", "4", "5"}) private Integer optimizeLevel; @Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.") diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java index 50ec4abb..b057709c 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/OverlayImageRequest.java @@ -3,23 +3,30 @@ package stirling.software.SPDF.model.api.misc; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class OverlayImageRequest extends PDFFile { @Schema(description = "The image file to be overlaid onto the PDF.") private MultipartFile imageFile; - @Schema(description = "The x-coordinate at which to place the top-left corner of the image.", example = "0") + @Schema( + description = "The x-coordinate at which to place the top-left corner of the image.", + example = "0") private float x; - @Schema(description = "The y-coordinate at which to place the top-left corner of the image.", example = "0") + @Schema( + description = "The y-coordinate at which to place the top-left corner of the image.", + example = "0") private float y; - @Schema(description = "Whether to overlay the image onto every page of the PDF.", example = "false") + @Schema( + description = "Whether to overlay the image onto every page of the PDF.", + example = "false") private boolean everyPage; } diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java index 392f8d54..7d3de3e6 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/ProcessPdfWithOcrRequest.java @@ -3,12 +3,13 @@ package stirling.software.SPDF.model.api.misc; import java.util.List; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class ProcessPdfWithOcrRequest extends PDFFile { @Schema(description = "List of languages to use in OCR processing") @@ -26,10 +27,15 @@ public class ProcessPdfWithOcrRequest extends PDFFile { @Schema(description = "Clean the final output if set to true") private boolean cleanFinal; - @Schema(description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", allowableValues = {"skip-text", "force-ocr", "Normal"}) + @Schema( + description = "Specify the OCR type, e.g., 'skip-text', 'force-ocr', or 'Normal'", + allowableValues = {"skip-text", "force-ocr", "Normal"}) private String ocrType; - @Schema(description = "Specify the OCR render type, either 'hocr' or 'sandwich'", allowableValues = {"hocr", "sandwich"}, defaultValue = "hocr") + @Schema( + description = "Specify the OCR render type, either 'hocr' or 'sandwich'", + allowableValues = {"hocr", "sandwich"}, + defaultValue = "hocr") private String ocrRenderType = "hocr"; @Schema(description = "Remove images from the output PDF if set to true") diff --git a/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java b/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java index 0d2e11c7..7177449f 100644 --- a/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/misc/RemoveBlankPagesRequest.java @@ -1,17 +1,24 @@ package stirling.software.SPDF.model.api.misc; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RemoveBlankPagesRequest extends PDFFile { - @Schema(description = "The threshold value to determine blank pages", example = "10", defaultValue = "10") + @Schema( + description = "The threshold value to determine blank pages", + example = "10", + defaultValue = "10") private int threshold = 10; - @Schema(description = "The percentage of white color on a page to consider it as blank", example = "99.9", defaultValue = "99.9") + @Schema( + description = "The percentage of white color on a page to consider it as blank", + example = "99.9", + defaultValue = "99.9") private float whitePercent = 99.9f; } diff --git a/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java index ea83e470..99cdfc36 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/AddPasswordRequest.java @@ -1,30 +1,44 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddPasswordRequest extends PDFFile { - @Schema(description = "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", defaultValue = "") + @Schema( + description = + "The owner password to be added to the PDF file (Restricts what can be done with the document once it is opened)", + defaultValue = "") private String ownerPassword; - @Schema(description = "The password to be added to the PDF file (Restricts the opening of the document itself.)", defaultValue = "") + @Schema( + description = + "The password to be added to the PDF file (Restricts the opening of the document itself.)", + defaultValue = "") private String password; - @Schema(description = "The length of the encryption key", allowableValues = {"40", "128", "256"}, defaultValue = "256") + @Schema( + description = "The length of the encryption key", + allowableValues = {"40", "128", "256"}, + defaultValue = "256") private int keyLength = 256; @Schema(description = "Whether the document assembly is allowed", example = "false") private boolean canAssembleDocument; - @Schema(description = "Whether content extraction for accessibility is allowed", example = "false") + @Schema( + description = "Whether content extraction for accessibility is allowed", + example = "false") private boolean canExtractContent; - @Schema(description = "Whether content extraction for accessibility is allowed", example = "false") + @Schema( + description = "Whether content extraction for accessibility is allowed", + example = "false") private boolean canExtractForAccessibility; @Schema(description = "Whether form filling is allowed", example = "false") diff --git a/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java index cd800948..861568bf 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/AddWatermarkRequest.java @@ -3,17 +3,19 @@ package stirling.software.SPDF.model.api.security; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class AddWatermarkRequest extends PDFFile { - @Schema(description = "The watermark type (text or image)", - allowableValues = {"text", "image"}, - required = true) + @Schema( + description = "The watermark type (text or image)", + allowableValues = {"text", "image"}, + required = true) private String watermarkType; @Schema(description = "The watermark text") @@ -22,8 +24,9 @@ public class AddWatermarkRequest extends PDFFile { @Schema(description = "The watermark image") private MultipartFile watermarkImage; - @Schema(description = "The selected alphabet", - allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, + @Schema( + description = "The selected alphabet", + allowableValues = {"roman", "arabic", "japanese", "korean", "chinese"}, defaultValue = "roman") private String alphabet = "roman"; diff --git a/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java index 94d04d1e..7f31abbb 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/PDFPasswordRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class PDFPasswordRequest extends PDFFile { @Schema(description = "The password of the PDF file", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java index 1966c53a..fce57ff5 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/RedactPdfRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class RedactPdfRequest extends PDFFile { @Schema(description = "List of text to redact from the PDF", type = "string", required = true) diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java index 98c7743f..0e12dfe7 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SanitizePdfRequest.java @@ -1,12 +1,13 @@ package stirling.software.SPDF.model.api.security; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SanitizePdfRequest extends PDFFile { @Schema(description = "Remove JavaScript actions from the PDF", defaultValue = "false") diff --git a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java index 8e537c6a..a1fc2fce 100644 --- a/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java +++ b/src/main/java/stirling/software/SPDF/model/api/security/SignPDFWithCertRequest.java @@ -3,18 +3,23 @@ package stirling.software.SPDF.model.api.security; import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; import lombok.EqualsAndHashCode; import stirling.software.SPDF.model.api.PDFFile; @Data -@EqualsAndHashCode(callSuper=true) +@EqualsAndHashCode(callSuper = true) public class SignPDFWithCertRequest extends PDFFile { - @Schema(description = "The type of the digital certificate", allowableValues = { "PKCS12", "PEM" }) + @Schema( + description = "The type of the digital certificate", + allowableValues = {"PKCS12", "PEM"}) private String certType; - @Schema(description = "The private key for the digital certificate (required for PEM type certificates)") + @Schema( + description = + "The private key for the digital certificate (required for PEM type certificates)") private MultipartFile privateKeyFile; @Schema(description = "The digital certificate (required for PEM type certificates)") @@ -38,6 +43,8 @@ public class SignPDFWithCertRequest extends PDFFile { @Schema(description = "The name of the signer") private String name; - @Schema(description = "The page number where the signature should be visible. This is required if showSignature is set to true") + @Schema( + description = + "The page number where the signature should be visible. This is required if showSignature is set to true") private Integer pageNumber; } diff --git a/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java b/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java index 0f49af76..a710dbd5 100644 --- a/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/ImageFinder.java @@ -48,83 +48,84 @@ public class ImageFinder extends org.apache.pdfbox.contentstream.PDFGraphicsStre super.processOperator(operator, operands); } - @Override - public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { + // TODO Auto-generated method stub - @Override - public void drawImage(PDImage pdImage) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void clip(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void drawImage(PDImage pdImage) throws IOException { + // TODO Auto-generated method stub - @Override - public void moveTo(float x, float y) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void lineTo(float x, float y) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void clip(int windingRule) throws IOException { + // TODO Auto-generated method stub - @Override - public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public Point2D getCurrentPoint() throws IOException { - // TODO Auto-generated method stub - return null; - } + @Override + public void moveTo(float x, float y) throws IOException { + // TODO Auto-generated method stub - @Override - public void closePath() throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void endPath() throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void lineTo(float x, float y) throws IOException { + // TODO Auto-generated method stub - @Override - public void strokePath() throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void fillPath(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) + throws IOException { + // TODO Auto-generated method stub - @Override - public void fillAndStrokePath(int windingRule) throws IOException { - // TODO Auto-generated method stub - - } + } - @Override - public void shadingFill(COSName shadingName) throws IOException { - // TODO Auto-generated method stub - - } + @Override + public Point2D getCurrentPoint() throws IOException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void closePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void endPath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void strokePath() throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillPath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void fillAndStrokePath(int windingRule) throws IOException { + // TODO Auto-generated method stub + + } + + @Override + public void shadingFill(COSName shadingName) throws IOException { + // TODO Auto-generated method stub + + } // ... rest of the overridden methods } diff --git a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java index f7eb9e3f..cdfb5501 100644 --- a/src/main/java/stirling/software/SPDF/pdf/TextFinder.java +++ b/src/main/java/stirling/software/SPDF/pdf/TextFinder.java @@ -1,4 +1,5 @@ package stirling.software.SPDF.pdf; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -13,78 +14,80 @@ import stirling.software.SPDF.model.PDFText; public class TextFinder extends PDFTextStripper { - private final String searchText; - private final boolean useRegex; - private final boolean wholeWordSearch; - private final List textOccurrences = new ArrayList<>(); + private final String searchText; + private final boolean useRegex; + private final boolean wholeWordSearch; + private final List textOccurrences = new ArrayList<>(); - public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) throws IOException { - this.searchText = searchText.toLowerCase(); - this.useRegex = useRegex; - this.wholeWordSearch = wholeWordSearch; - setSortByPosition(true); - } + public TextFinder(String searchText, boolean useRegex, boolean wholeWordSearch) + throws IOException { + this.searchText = searchText.toLowerCase(); + this.useRegex = useRegex; + this.wholeWordSearch = wholeWordSearch; + setSortByPosition(true); + } - private List findOccurrencesInText(String searchText, String content) { - List indexes = new ArrayList<>(); - Pattern pattern; + private List findOccurrencesInText(String searchText, String content) { + List indexes = new ArrayList<>(); + Pattern pattern; - if (useRegex) { - // Use regex-based search - pattern = wholeWordSearch - ? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)") - : Pattern.compile(searchText); - } else { - // Use normal text search - pattern = wholeWordSearch - ? Pattern.compile("(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)") - : Pattern.compile(Pattern.quote(searchText)); - } + if (useRegex) { + // Use regex-based search + pattern = + wholeWordSearch + ? Pattern.compile("(\\b|_|\\.)" + searchText + "(\\b|_|\\.)") + : Pattern.compile(searchText); + } else { + // Use normal text search + pattern = + wholeWordSearch + ? Pattern.compile( + "(\\b|_|\\.)" + Pattern.quote(searchText) + "(\\b|_|\\.)") + : Pattern.compile(Pattern.quote(searchText)); + } - Matcher matcher = pattern.matcher(content); - while (matcher.find()) { - indexes.add(matcher.start()); - } - return indexes; - } - - @Override - protected void writeString(String text, List textPositions) { - for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) { - if (index + searchText.length() <= textPositions.size()) { - // Initial values based on the first character - TextPosition first = textPositions.get(index); - float minX = first.getX(); - float minY = first.getY(); - float maxX = first.getX() + first.getWidth(); - float maxY = first.getY() + first.getHeight(); + Matcher matcher = pattern.matcher(content); + while (matcher.find()) { + indexes.add(matcher.start()); + } + return indexes; + } - // Loop over the rest of the characters and adjust bounding box values - for (int i = index; i < index + searchText.length(); i++) { - TextPosition position = textPositions.get(i); - minX = Math.min(minX, position.getX()); - minY = Math.min(minY, position.getY()); - maxX = Math.max(maxX, position.getX() + position.getWidth()); - maxY = Math.max(maxY, position.getY() + position.getHeight()); - } + @Override + protected void writeString(String text, List textPositions) { + for (Integer index : findOccurrencesInText(searchText, text.toLowerCase())) { + if (index + searchText.length() <= textPositions.size()) { + // Initial values based on the first character + TextPosition first = textPositions.get(index); + float minX = first.getX(); + float minY = first.getY(); + float maxX = first.getX() + first.getWidth(); + float maxY = first.getY() + first.getHeight(); - textOccurrences.add(new PDFText( - getCurrentPageNo() - 1, - minX, - minY, - maxX, - maxY, - text - )); - } - } - } + // Loop over the rest of the characters and adjust bounding box values + for (int i = index; i < index + searchText.length(); i++) { + TextPosition position = textPositions.get(i); + minX = Math.min(minX, position.getX()); + minY = Math.min(minY, position.getY()); + maxX = Math.max(maxX, position.getX() + position.getWidth()); + maxY = Math.max(maxY, position.getY() + position.getHeight()); + } - public List getTextLocations(PDDocument document) throws Exception { - this.getText(document); - System.out.println("Found " + textOccurrences.size() + " occurrences of '" + searchText + "' in the document."); + textOccurrences.add( + new PDFText(getCurrentPageNo() - 1, minX, minY, maxX, maxY, text)); + } + } + } - return textOccurrences; - } + public List getTextLocations(PDDocument document) throws Exception { + this.getText(document); + System.out.println( + "Found " + + textOccurrences.size() + + " occurrences of '" + + searchText + + "' in the document."); -} \ No newline at end of file + return textOccurrences; + } +} diff --git a/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java index 62f546b8..bbf32a07 100644 --- a/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/AuthorityRepository.java @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import stirling.software.SPDF.model.Authority; -public interface AuthorityRepository extends JpaRepository { - //Set findByUsername(String username); +public interface AuthorityRepository extends JpaRepository { + // Set findByUsername(String username); Set findByUser_Username(String username); } diff --git a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java index e7753903..a901d3c3 100644 --- a/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java +++ b/src/main/java/stirling/software/SPDF/repository/JPATokenRepositoryImpl.java @@ -10,8 +10,7 @@ import stirling.software.SPDF.model.PersistentLogin; public class JPATokenRepositoryImpl implements PersistentTokenRepository { - @Autowired - private PersistentLoginRepository persistentLoginRepository; + @Autowired private PersistentLoginRepository persistentLoginRepository; @Override public void createNewToken(PersistentRememberMeToken token) { @@ -37,7 +36,8 @@ public class JPATokenRepositoryImpl implements PersistentTokenRepository { public PersistentRememberMeToken getTokenForSeries(String seriesId) { PersistentLogin token = persistentLoginRepository.findById(seriesId).orElse(null); if (token != null) { - return new PersistentRememberMeToken(token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed()); + return new PersistentRememberMeToken( + token.getUsername(), token.getSeries(), token.getToken(), token.getLastUsed()); } return null; } diff --git a/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java b/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java index 10c1acae..31841a57 100644 --- a/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/PersistentLoginRepository.java @@ -4,5 +4,4 @@ import org.springframework.data.jpa.repository.JpaRepository; import stirling.software.SPDF.model.PersistentLogin; -public interface PersistentLoginRepository extends JpaRepository { -} +public interface PersistentLoginRepository extends JpaRepository {} diff --git a/src/main/java/stirling/software/SPDF/repository/UserRepository.java b/src/main/java/stirling/software/SPDF/repository/UserRepository.java index 744953d7..d63c4cba 100644 --- a/src/main/java/stirling/software/SPDF/repository/UserRepository.java +++ b/src/main/java/stirling/software/SPDF/repository/UserRepository.java @@ -8,6 +8,6 @@ import stirling.software.SPDF.model.User; public interface UserRepository extends JpaRepository { Optional findByUsername(String username); + User findByApiKey(String apiKey); } - diff --git a/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java b/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java index 493ce63e..e84e8d88 100644 --- a/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ErrorUtils.java @@ -28,5 +28,4 @@ public class ErrorUtils { modelAndView.addObject("stackTrace", stackTrace); return modelAndView; } - } diff --git a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java index 9515a3ac..5e6825dd 100644 --- a/src/main/java/stirling/software/SPDF/utils/FileToPdf.java +++ b/src/main/java/stirling/software/SPDF/utils/FileToPdf.java @@ -14,82 +14,87 @@ import java.util.zip.ZipInputStream; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; public class FileToPdf { - public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName) throws IOException, InterruptedException { - - Path tempOutputFile = Files.createTempFile("output_", ".pdf"); - Path tempInputFile = null; - byte[] pdfBytes; - try { - if (fileName.endsWith(".html")) { - tempInputFile = Files.createTempFile("input_", ".html"); - Files.write(tempInputFile, fileBytes); - } else { - tempInputFile = unzipAndGetMainHtml(fileBytes); - } - - List command = new ArrayList<>(); - command.add("weasyprint"); - command.add(tempInputFile.toString()); - command.add(tempOutputFile.toString()); - ProcessExecutorResult returnCode; - if (fileName.endsWith(".zip")) { - returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command, tempInputFile.getParent().toFile()); - } else { - - returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) - .runCommandWithOutputHandling(command); - } - - pdfBytes = Files.readAllBytes(tempOutputFile); - } finally { - // Clean up temporary files - Files.delete(tempOutputFile); - Files.delete(tempInputFile); - - if (fileName.endsWith(".zip")) { - GeneralUtils.deleteDirectory(tempInputFile.getParent()); - } - } - - return pdfBytes; - } - + public static byte[] convertHtmlToPdf(byte[] fileBytes, String fileName) + throws IOException, InterruptedException { - private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException { - Path tempDirectory = Files.createTempDirectory("unzipped_"); - try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { - ZipEntry entry = zipIn.getNextEntry(); - while (entry != null) { - Path filePath = tempDirectory.resolve(entry.getName()); - if (entry.isDirectory()) { - Files.createDirectories(filePath); // Explicitly create the directory structure - } else { - Files.createDirectories(filePath.getParent()); // Create parent directories if they don't exist - Files.copy(zipIn, filePath); - } - zipIn.closeEntry(); - entry = zipIn.getNextEntry(); - } - } + Path tempOutputFile = Files.createTempFile("output_", ".pdf"); + Path tempInputFile = null; + byte[] pdfBytes; + try { + if (fileName.endsWith(".html")) { + tempInputFile = Files.createTempFile("input_", ".html"); + Files.write(tempInputFile, fileBytes); + } else { + tempInputFile = unzipAndGetMainHtml(fileBytes); + } - //search for the main HTML file. - try (Stream walk = Files.walk(tempDirectory)) { - List htmlFiles = walk.filter(file -> file.toString().endsWith(".html")) - .collect(Collectors.toList()); + List command = new ArrayList<>(); + command.add("weasyprint"); + command.add(tempInputFile.toString()); + command.add(tempOutputFile.toString()); + ProcessExecutorResult returnCode; + if (fileName.endsWith(".zip")) { + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling( + command, tempInputFile.getParent().toFile()); + } else { - if (htmlFiles.isEmpty()) { - throw new IOException("No HTML files found in the unzipped directory."); - } + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.WEASYPRINT) + .runCommandWithOutputHandling(command); + } - // Prioritize 'index.html' if it exists, otherwise use the first .html file - for (Path htmlFile : htmlFiles) { - if (htmlFile.getFileName().toString().equals("index.html")) { - return htmlFile; - } - } + pdfBytes = Files.readAllBytes(tempOutputFile); + } finally { + // Clean up temporary files + Files.delete(tempOutputFile); + Files.delete(tempInputFile); - return htmlFiles.get(0); - } - } + if (fileName.endsWith(".zip")) { + GeneralUtils.deleteDirectory(tempInputFile.getParent()); + } + } + + return pdfBytes; + } + + private static Path unzipAndGetMainHtml(byte[] fileBytes) throws IOException { + Path tempDirectory = Files.createTempDirectory("unzipped_"); + try (ZipInputStream zipIn = new ZipInputStream(new ByteArrayInputStream(fileBytes))) { + ZipEntry entry = zipIn.getNextEntry(); + while (entry != null) { + Path filePath = tempDirectory.resolve(entry.getName()); + if (entry.isDirectory()) { + Files.createDirectories(filePath); // Explicitly create the directory structure + } else { + Files.createDirectories( + filePath.getParent()); // Create parent directories if they don't exist + Files.copy(zipIn, filePath); + } + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + } + + // search for the main HTML file. + try (Stream walk = Files.walk(tempDirectory)) { + List htmlFiles = + walk.filter(file -> file.toString().endsWith(".html")) + .collect(Collectors.toList()); + + if (htmlFiles.isEmpty()) { + throw new IOException("No HTML files found in the unzipped directory."); + } + + // Prioritize 'index.html' if it exists, otherwise use the first .html file + for (Path htmlFile : htmlFiles) { + if (htmlFile.getFileName().toString().equals("index.html")) { + return htmlFile; + } + } + + return htmlFiles.get(0); + } + } } diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index 6de7df14..a1e177e4 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -16,46 +16,50 @@ import java.util.ArrayList; import java.util.List; import org.springframework.web.multipart.MultipartFile; + public class GeneralUtils { - public static void deleteDirectory(Path path) throws IOException { - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } + public static void deleteDirectory(Path path) throws IOException { + Files.walkFileTree( + path, + new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); } - public static String convertToFileName(String name) { + public static String convertToFileName(String name) { String safeName = name.replaceAll("[^a-zA-Z0-9]", "_"); if (safeName.length() > 50) { safeName = safeName.substring(0, 50); } return safeName; } - - - public static boolean isValidURL(String urlStr) { - try { - new URL(urlStr); - return true; - } catch (MalformedURLException e) { - return false; - } - } - public static File multipartToFile(MultipartFile multipart) throws IOException { + public static boolean isValidURL(String urlStr) { + try { + new URL(urlStr); + return true; + } catch (MalformedURLException e) { + return false; + } + } + + public static File multipartToFile(MultipartFile multipart) throws IOException { Path tempFile = Files.createTempFile("overlay-", ".pdf"); - try (InputStream in = multipart.getInputStream(); - FileOutputStream out = new FileOutputStream(tempFile.toFile())) { + try (InputStream in = multipart.getInputStream(); + FileOutputStream out = new FileOutputStream(tempFile.toFile())) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = in.read(buffer)) != -1) { @@ -64,110 +68,119 @@ public class GeneralUtils { } return tempFile.toFile(); } - - public static Long convertSizeToBytes(String sizeStr) { - if (sizeStr == null) { - return null; - } - - sizeStr = sizeStr.trim().toUpperCase(); - try { - if (sizeStr.endsWith("KB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); - } else if (sizeStr.endsWith("MB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024); - } else if (sizeStr.endsWith("GB")) { - return (long) (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024 * 1024 * 1024); - } else if (sizeStr.endsWith("B")) { - return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); - } else { - // Assume MB if no unit is specified - return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); - } - } catch (NumberFormatException e) { - // The numeric part of the input string cannot be parsed, handle this case - } - - return null; - } - public static List parsePageString(String pageOrder, int totalPages) { - return parsePageList(pageOrder.split(","), totalPages); - } - public static List parsePageList(String[] pageOrderArr, int totalPages) { - List newPageOrder = new ArrayList<>(); + public static Long convertSizeToBytes(String sizeStr) { + if (sizeStr == null) { + return null; + } - // loop through the page order array - for (String element : pageOrderArr) { - if (element.equalsIgnoreCase("all")) { - for (int i = 0; i < totalPages; i++) { - newPageOrder.add(i); - } - // As all pages are already added, no need to check further - break; - } - else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { - // Handle page order as a function - int coefficient = 0; - int constant = 0; - boolean coefficientExists = false; - boolean constantExists = false; + sizeStr = sizeStr.trim().toUpperCase(); + try { + if (sizeStr.endsWith("KB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) * 1024); + } else if (sizeStr.endsWith("MB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024); + } else if (sizeStr.endsWith("GB")) { + return (long) + (Double.parseDouble(sizeStr.substring(0, sizeStr.length() - 2)) + * 1024 + * 1024 + * 1024); + } else if (sizeStr.endsWith("B")) { + return Long.parseLong(sizeStr.substring(0, sizeStr.length() - 1)); + } else { + // Assume MB if no unit is specified + return (long) (Double.parseDouble(sizeStr) * 1024 * 1024); + } + } catch (NumberFormatException e) { + // The numeric part of the input string cannot be parsed, handle this case + } - if (element.contains("n")) { - String[] parts = element.split("n"); - if (!parts[0].equals("") && parts[0] != null) { - coefficient = Integer.parseInt(parts[0]); - coefficientExists = true; - } - if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { - constant = Integer.parseInt(parts[1]); - constantExists = true; - } - } else if (element.contains("+")) { - constant = Integer.parseInt(element.replace("+", "")); - constantExists = true; - } + return null; + } - for (int i = 1; i <= totalPages; i++) { - int pageNum = coefficientExists ? coefficient * i : i; - pageNum += constantExists ? constant : 0; + public static List parsePageString(String pageOrder, int totalPages) { + return parsePageList(pageOrder.split(","), totalPages); + } - if (pageNum <= totalPages && pageNum > 0) { - newPageOrder.add(pageNum - 1); - } - } - } else if (element.contains("-")) { - // split the range into start and end page - String[] range = element.split("-"); - int start = Integer.parseInt(range[0]); - int end = Integer.parseInt(range[1]); - // check if the end page is greater than total pages - if (end > totalPages) { - end = totalPages; - } - // loop through the range of pages - for (int j = start; j <= end; j++) { - // print the current index - newPageOrder.add(j - 1); - } - } else { - // if the element is a single page - newPageOrder.add(Integer.parseInt(element) - 1); - } - } + public static List parsePageList(String[] pageOrderArr, int totalPages) { + List newPageOrder = new ArrayList<>(); - return newPageOrder; - } - public static boolean createDir(String path) { - Path folder = Paths.get(path); - if (!Files.exists(folder)) { - try { - Files.createDirectories(folder); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - return true; - } + // loop through the page order array + for (String element : pageOrderArr) { + if (element.equalsIgnoreCase("all")) { + for (int i = 0; i < totalPages; i++) { + newPageOrder.add(i); + } + // As all pages are already added, no need to check further + break; + } else if (element.matches("\\d*n\\+?-?\\d*|\\d*\\+?n")) { + // Handle page order as a function + int coefficient = 0; + int constant = 0; + boolean coefficientExists = false; + boolean constantExists = false; + + if (element.contains("n")) { + String[] parts = element.split("n"); + if (!parts[0].equals("") && parts[0] != null) { + coefficient = Integer.parseInt(parts[0]); + coefficientExists = true; + } + if (parts.length > 1 && !parts[1].equals("") && parts[1] != null) { + constant = Integer.parseInt(parts[1]); + constantExists = true; + } + } else if (element.contains("+")) { + constant = Integer.parseInt(element.replace("+", "")); + constantExists = true; + } + + for (int i = 1; i <= totalPages; i++) { + int pageNum = coefficientExists ? coefficient * i : i; + pageNum += constantExists ? constant : 0; + + if (pageNum <= totalPages && pageNum > 0) { + newPageOrder.add(pageNum - 1); + } + } + } else if (element.contains("-")) { + // split the range into start and end page + String[] range = element.split("-"); + int start = Integer.parseInt(range[0]); + int end = Integer.parseInt(range[1]); + // check if the end page is greater than total pages + if (end > totalPages) { + end = totalPages; + } + // loop through the range of pages + for (int j = start; j <= end; j++) { + // print the current index + newPageOrder.add(j - 1); + } + } else { + // if the element is a single page + newPageOrder.add(Integer.parseInt(element) - 1); + } + } + + return newPageOrder; + } + + public static boolean createDir(String path) { + Path folder = Paths.get(path); + if (!Files.exists(folder)) { + try { + Files.createDirectories(folder); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + return true; + } } diff --git a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java index e34892d5..ede9c4f4 100644 --- a/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/ImageProcessingUtils.java @@ -4,22 +4,29 @@ import java.awt.image.BufferedImage; public class ImageProcessingUtils { - static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { - BufferedImage convertedImage; - switch (colorType) { - case "greyscale": - convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY); - convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - case "blackwhite": - convertedImage = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), BufferedImage.TYPE_BYTE_BINARY); - convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); - break; - default: // full color - convertedImage = sourceImage; - break; - } - return convertedImage; - } - + static BufferedImage convertColorType(BufferedImage sourceImage, String colorType) { + BufferedImage convertedImage; + switch (colorType) { + case "greyscale": + convertedImage = + new BufferedImage( + sourceImage.getWidth(), + sourceImage.getHeight(), + BufferedImage.TYPE_BYTE_GRAY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + case "blackwhite": + convertedImage = + new BufferedImage( + sourceImage.getWidth(), + sourceImage.getHeight(), + BufferedImage.TYPE_BYTE_BINARY); + convertedImage.getGraphics().drawImage(sourceImage, 0, 0, null); + break; + default: // full color + convertedImage = sourceImage; + break; + } + return convertedImage; + } } diff --git a/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java b/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java index 5718cd8a..397ff5bc 100644 --- a/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PDFManipulationUtils.java @@ -1,5 +1,3 @@ package stirling.software.SPDF.utils; -public class PDFManipulationUtils { - -} +public class PDFManipulationUtils {} diff --git a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java index af658f79..a7f751af 100644 --- a/src/main/java/stirling/software/SPDF/utils/PDFToFile.java +++ b/src/main/java/stirling/software/SPDF/utils/PDFToFile.java @@ -23,7 +23,9 @@ import org.springframework.web.multipart.MultipartFile; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; public class PDFToFile { - public ResponseEntity processPdfToOfficeFormat(MultipartFile inputFile, String outputFormat, String libreOfficeFilter) throws IOException, InterruptedException { + public ResponseEntity processPdfToOfficeFormat( + MultipartFile inputFile, String outputFormat, String libreOfficeFilter) + throws IOException, InterruptedException { if (!"application/pdf".equals(inputFile.getContentType())) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); @@ -34,7 +36,18 @@ public class PDFToFile { String pdfBaseName = originalPdfFileName.substring(0, originalPdfFileName.lastIndexOf('.')); // Validate output format - List allowedFormats = Arrays.asList("doc", "docx", "odt", "ppt", "pptx", "odp", "rtf", "html", "xml", "txt:Text"); + List allowedFormats = + Arrays.asList( + "doc", + "docx", + "odt", + "ppt", + "pptx", + "odp", + "rtf", + "html", + "xml", + "txt:Text"); if (!allowedFormats.contains(outputFormat)) { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } @@ -47,15 +60,26 @@ public class PDFToFile { try { // Save the uploaded file to a temporary location tempInputFile = Files.createTempFile("input_", ".pdf"); - Files.copy(inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); + Files.copy( + inputFile.getInputStream(), tempInputFile, StandardCopyOption.REPLACE_EXISTING); // Prepare the output directory tempOutputDir = Files.createTempDirectory("output_"); // Run the LibreOffice command - List command = new ArrayList<>( - Arrays.asList("soffice", "--infilter=" + libreOfficeFilter, "--convert-to", outputFormat, "--outdir", tempOutputDir.toString(), tempInputFile.toString())); - ProcessExecutorResult returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE).runCommandWithOutputHandling(command); + List command = + new ArrayList<>( + Arrays.asList( + "soffice", + "--infilter=" + libreOfficeFilter, + "--convert-to", + outputFormat, + "--outdir", + tempOutputDir.toString(), + tempInputFile.toString())); + ProcessExecutorResult returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.LIBRE_OFFICE) + .runCommandWithOutputHandling(command); // Get output files List outputFiles = Arrays.asList(tempOutputDir.toFile().listFiles()); @@ -89,11 +113,10 @@ public class PDFToFile { } finally { // Clean up the temporary files - if (tempInputFile != null) - Files.delete(tempInputFile); - if (tempOutputDir != null) - FileUtils.deleteDirectory(tempOutputDir.toFile()); + if (tempInputFile != null) Files.delete(tempInputFile); + if (tempOutputDir != null) FileUtils.deleteDirectory(tempOutputDir.toFile()); } - return WebResponseUtils.bytesToWebResponse(fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); + return WebResponseUtils.bytesToWebResponse( + fileBytes, fileName, MediaType.APPLICATION_OCTET_STREAM); } } diff --git a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java index 2be68d08..677bafd1 100644 --- a/src/main/java/stirling/software/SPDF/utils/PdfUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/PdfUtils.java @@ -4,22 +4,16 @@ import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import javax.imageio.ImageIO; import javax.imageio.IIOImage; +import javax.imageio.ImageIO; import javax.imageio.ImageReader; -import javax.imageio.ImageWriter; import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; import javax.imageio.stream.ImageOutputStream; import org.apache.pdfbox.pdmodel.PDDocument; @@ -42,38 +36,35 @@ public class PdfUtils { private static final Logger logger = LoggerFactory.getLogger(PdfUtils.class); + public static PDRectangle textToPageSize(String size) { + switch (size.toUpperCase()) { + case "A0": + return PDRectangle.A0; + case "A1": + return PDRectangle.A1; + case "A2": + return PDRectangle.A2; + case "A3": + return PDRectangle.A3; + case "A4": + return PDRectangle.A4; + case "A5": + return PDRectangle.A5; + case "A6": + return PDRectangle.A6; + case "LETTER": + return PDRectangle.LETTER; + case "LEGAL": + return PDRectangle.LEGAL; + default: + throw new IllegalArgumentException("Invalid standard page size: " + size); + } + } - public static PDRectangle textToPageSize(String size) { - switch (size.toUpperCase()) { - case "A0": - return PDRectangle.A0; - case "A1": - return PDRectangle.A1; - case "A2": - return PDRectangle.A2; - case "A3": - return PDRectangle.A3; - case "A4": - return PDRectangle.A4; - case "A5": - return PDRectangle.A5; - case "A6": - return PDRectangle.A6; - case "LETTER": - return PDRectangle.LETTER; - case "LEGAL": - return PDRectangle.LEGAL; - default: - throw new IllegalArgumentException("Invalid standard page size: " + size); - } - } - - - - public static boolean hasImages(PDDocument document, String pagesToCheck) throws IOException { String[] pageOrderArr = pagesToCheck.split(","); - List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); for (int pageNumber : pageList) { PDPage page = document.getPage(pageNumber); @@ -85,9 +76,11 @@ public class PdfUtils { return false; } - public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) throws IOException { + public static boolean hasText(PDDocument document, String pageNumbersToCheck, String phrase) + throws IOException { String[] pageOrderArr = pageNumbersToCheck.split(","); - List pageList = GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); + List pageList = + GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages()); for (int pageNumber : pageList) { PDPage page = document.getPage(pageNumber); @@ -99,15 +92,11 @@ public class PdfUtils { return false; } - public static boolean hasImagesOnPage(PDPage page) throws IOException { ImageFinder imageFinder = new ImageFinder(page); imageFinder.processPage(page); return imageFinder.hasImages(); } - - - public static boolean hasTextOnPage(PDPage page, String phrase) throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); @@ -118,12 +107,12 @@ public class PdfUtils { return pageText.contains(phrase); } - - public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) throws IOException { + public boolean containsTextInFile(PDDocument pdfDocument, String text, String pagesToCheck) + throws IOException { PDFTextStripper textStripper = new PDFTextStripper(); String pdfText = ""; - if(pagesToCheck == null || pagesToCheck.equals("all")) { + if (pagesToCheck == null || pagesToCheck.equals("all")) { pdfText = textStripper.getText(pdfDocument); } else { // remove whitespaces @@ -157,15 +146,12 @@ public class PdfUtils { return pdfText.contains(text); } - - - - - public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) throws IOException { + public boolean pageCount(PDDocument pdfDocument, int pageCount, String comparator) + throws IOException { int actualPageCount = pdfDocument.getNumberOfPages(); pdfDocument.close(); - switch(comparator.toLowerCase()) { + switch (comparator.toLowerCase()) { case "greater": return actualPageCount > pageCount; case "equal": @@ -173,7 +159,8 @@ public class PdfUtils { case "less": return actualPageCount < pageCount; default: - throw new IllegalArgumentException("Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); + throw new IllegalArgumentException( + "Invalid comparator. Only 'greater', 'equal', and 'less' are supported."); } } @@ -194,12 +181,15 @@ public class PdfUtils { // Checks if the actual page size matches the expected page size return actualPageWidth == expectedPageWidth && actualPageHeight == expectedPageHeight; } - - - - - - public static byte[] convertFromPdf(byte[] inputStream, String imageType, ImageType colorType, boolean singleImage, int DPI, String filename) throws IOException, Exception { + + public static byte[] convertFromPdf( + byte[] inputStream, + String imageType, + ImageType colorType, + boolean singleImage, + int DPI, + String filename) + throws IOException, Exception { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(inputStream))) { PDFRenderer pdfRenderer = new PDFRenderer(document); int pageCount = document.getNumberOfPages(); @@ -208,7 +198,8 @@ public class PdfUtils { ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (singleImage) { - if (imageType.toLowerCase().equals("tiff") || imageType.toLowerCase().equals("tif")) { + if (imageType.toLowerCase().equals("tiff") + || imageType.toLowerCase().equals("tif")) { // Write the images to the output stream as a TIFF with multiple frames ImageWriter writer = ImageIO.getImageWritersByFormatName("tiff").next(); ImageWriteParam param = writer.getDefaultWriteParam(); @@ -232,13 +223,17 @@ public class PdfUtils { } else { // Combine all images into a single big image BufferedImage image = pdfRenderer.renderImageWithDPI(0, DPI, colorType); - BufferedImage combined = new BufferedImage(image.getWidth(), image.getHeight() * pageCount, BufferedImage.TYPE_INT_RGB); + BufferedImage combined = + new BufferedImage( + image.getWidth(), + image.getHeight() * pageCount, + BufferedImage.TYPE_INT_RGB); Graphics g = combined.getGraphics(); for (int i = 0; i < pageCount; ++i) { if (i != 0) { image = pdfRenderer.renderImageWithDPI(i, DPI, colorType); - } + } g.drawImage(image, 0, i * image.getHeight(), null); } @@ -257,7 +252,12 @@ public class PdfUtils { ImageIO.write(image, imageType, baosImage); // Add the image to the zip file - zos.putNextEntry(new ZipEntry(String.format(filename + "_%d.%s", i + 1, imageType.toLowerCase()))); + zos.putNextEntry( + new ZipEntry( + String.format( + filename + "_%d.%s", + i + 1, + imageType.toLowerCase()))); zos.write(baosImage.toByteArray()); } } @@ -272,28 +272,37 @@ public class PdfUtils { throw e; } } - public static byte[] imageToPdf(MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) throws IOException { + + public static byte[] imageToPdf( + MultipartFile[] files, String fitOption, boolean autoRotate, String colorType) + throws IOException { try (PDDocument doc = new PDDocument()) { for (MultipartFile file : files) { - String contentType = file.getContentType(); + String contentType = file.getContentType(); String originalFilename = file.getOriginalFilename(); - if (originalFilename != null && (originalFilename.toLowerCase().endsWith(".tiff") || originalFilename.toLowerCase().endsWith(".tif")) ) { + if (originalFilename != null + && (originalFilename.toLowerCase().endsWith(".tiff") + || originalFilename.toLowerCase().endsWith(".tif"))) { ImageReader reader = ImageIO.getImageReadersByFormatName("tiff").next(); reader.setInput(ImageIO.createImageInputStream(file.getInputStream())); int numPages = reader.getNumImages(true); for (int i = 0; i < numPages; i++) { BufferedImage pageImage = reader.read(i); - BufferedImage convertedImage = ImageProcessingUtils.convertColorType(pageImage, colorType); - PDImageXObject pdImage = LosslessFactory.createFromImage(doc, convertedImage); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(pageImage, colorType); + PDImageXObject pdImage = + LosslessFactory.createFromImage(doc, convertedImage); addImageToDocument(doc, pdImage, fitOption, autoRotate); } } else { BufferedImage image = ImageIO.read(file.getInputStream()); - BufferedImage convertedImage = ImageProcessingUtils.convertColorType(image, colorType); + BufferedImage convertedImage = + ImageProcessingUtils.convertColorType(image, colorType); // Use JPEGFactory if it's JPEG since JPEG is lossy - PDImageXObject pdImage = (contentType != null && contentType.equals("image/jpeg")) - ? JPEGFactory.createFromImage(doc, convertedImage) - : LosslessFactory.createFromImage(doc, convertedImage); + PDImageXObject pdImage = + (contentType != null && contentType.equals("image/jpeg")) + ? JPEGFactory.createFromImage(doc, convertedImage) + : LosslessFactory.createFromImage(doc, convertedImage); addImageToDocument(doc, pdImage, fitOption, autoRotate); } } @@ -304,11 +313,13 @@ public class PdfUtils { } } - private static void addImageToDocument(PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) throws IOException { + private static void addImageToDocument( + PDDocument doc, PDImageXObject image, String fitOption, boolean autoRotate) + throws IOException { boolean imageIsLandscape = image.getWidth() > image.getHeight(); PDRectangle pageSize = PDRectangle.A4; - System.out.println(fitOption); + System.out.println(fitOption); if (autoRotate && imageIsLandscape) { pageSize = new PDRectangle(pageSize.getHeight(), pageSize.getWidth()); @@ -340,7 +351,12 @@ public class PdfUtils { float xPos = (pageWidth - (image.getWidth() * scaleFactor)) / 2; float yPos = (pageHeight - (image.getHeight() * scaleFactor)) / 2; - contentStream.drawImage(image, xPos, yPos, image.getWidth() * scaleFactor, image.getHeight() * scaleFactor); + contentStream.drawImage( + image, + xPos, + yPos, + image.getWidth() * scaleFactor, + image.getHeight() * scaleFactor); } } catch (IOException e) { logger.error("Error adding image to PDF", e); @@ -348,8 +364,9 @@ public class PdfUtils { } } - - public static byte[] overlayImage(byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) throws IOException { + public static byte[] overlayImage( + byte[] pdfBytes, byte[] imageBytes, float x, float y, boolean everyPage) + throws IOException { PDDocument document = PDDocument.load(new ByteArrayInputStream(pdfBytes)); @@ -357,7 +374,9 @@ public class PdfUtils { int pages = document.getNumberOfPages(); for (int i = 0; i < pages; i++) { PDPage page = document.getPage(i); - try (PDPageContentStream contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true)) { + try (PDPageContentStream contentStream = + new PDPageContentStream( + document, page, PDPageContentStream.AppendMode.APPEND, true)) { // Create an image object from the image bytes PDImageXObject image = PDImageXObject.createFromByteArray(document, imageBytes, ""); // Draw the image onto the page at the specified x and y coordinates @@ -371,7 +390,6 @@ public class PdfUtils { logger.error("Error overlaying image onto PDF", e); throw e; } - } // Create a ByteArrayOutputStream to save the PDF to ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -379,8 +397,4 @@ public class PdfUtils { logger.info("PDF successfully saved to byte array"); return baos.toByteArray(); } - - - - } diff --git a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java index fe5c6717..385f3b80 100644 --- a/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java +++ b/src/main/java/stirling/software/SPDF/utils/ProcessExecutor.java @@ -14,22 +14,29 @@ import java.util.concurrent.Semaphore; public class ProcessExecutor { public enum Processes { - LIBRE_OFFICE, OCR_MY_PDF, PYTHON_OPENCV, GHOSTSCRIPT, WEASYPRINT + LIBRE_OFFICE, + OCR_MY_PDF, + PYTHON_OPENCV, + GHOSTSCRIPT, + WEASYPRINT } private static final Map instances = new ConcurrentHashMap<>(); public static ProcessExecutor getInstance(Processes processType) { - return instances.computeIfAbsent(processType, key -> { - int semaphoreLimit = switch (key) { - case LIBRE_OFFICE -> 1; - case OCR_MY_PDF -> 2; - case PYTHON_OPENCV -> 8; - case GHOSTSCRIPT -> 16; - case WEASYPRINT -> 16; - }; - return new ProcessExecutor(semaphoreLimit); - }); + return instances.computeIfAbsent( + processType, + key -> { + int semaphoreLimit = + switch (key) { + case LIBRE_OFFICE -> 1; + case OCR_MY_PDF -> 2; + case PYTHON_OPENCV -> 8; + case GHOSTSCRIPT -> 16; + case WEASYPRINT -> 16; + }; + return new ProcessExecutor(semaphoreLimit); + }); } private final Semaphore semaphore; @@ -37,10 +44,14 @@ public class ProcessExecutor { private ProcessExecutor(int semaphoreLimit) { this.semaphore = new Semaphore(semaphoreLimit); } - public ProcessExecutorResult runCommandWithOutputHandling(List command) throws IOException, InterruptedException { - return runCommandWithOutputHandling(command, null); + + public ProcessExecutorResult runCommandWithOutputHandling(List command) + throws IOException, InterruptedException { + return runCommandWithOutputHandling(command, null); } - public ProcessExecutorResult runCommandWithOutputHandling(List command, File workingDirectory) throws IOException, InterruptedException { + + public ProcessExecutorResult runCommandWithOutputHandling( + List command, File workingDirectory) throws IOException, InterruptedException { int exitCode = 1; String messages = ""; semaphore.acquire(); @@ -48,7 +59,7 @@ public class ProcessExecutor { System.out.print("Running command: " + String.join(" ", command)); ProcessBuilder processBuilder = new ProcessBuilder(command); - + // Use the working directory if it's set if (workingDirectory != null) { processBuilder.directory(workingDirectory); @@ -59,27 +70,39 @@ public class ProcessExecutor { List errorLines = new ArrayList<>(); List outputLines = new ArrayList<>(); - Thread errorReaderThread = new Thread(() -> { - try (BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = errorReader.readLine()) != null) { - errorLines.add(line); - } - } catch (IOException e) { - e.printStackTrace(); - } - }); + Thread errorReaderThread = + new Thread( + () -> { + try (BufferedReader errorReader = + new BufferedReader( + new InputStreamReader( + process.getErrorStream(), + StandardCharsets.UTF_8))) { + String line; + while ((line = errorReader.readLine()) != null) { + errorLines.add(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); - Thread outputReaderThread = new Thread(() -> { - try (BufferedReader outputReader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { - String line; - while ((line = outputReader.readLine()) != null) { - outputLines.add(line); - } - } catch (IOException e) { - e.printStackTrace(); - } - }); + Thread outputReaderThread = + new Thread( + () -> { + try (BufferedReader outputReader = + new BufferedReader( + new InputStreamReader( + process.getInputStream(), + StandardCharsets.UTF_8))) { + String line; + while ((line = outputReader.readLine()) != null) { + outputLines.add(line); + } + } catch (IOException e) { + e.printStackTrace(); + } + }); errorReaderThread.start(); outputReaderThread.start(); @@ -90,7 +113,7 @@ public class ProcessExecutor { // Wait for the reader threads to finish errorReaderThread.join(); outputReaderThread.join(); - + if (outputLines.size() > 0) { String outputMessage = String.join("\n", outputLines); messages += outputMessage; @@ -102,7 +125,11 @@ public class ProcessExecutor { messages += errorMessage; System.out.println("Command error output:\n" + errorMessage); if (exitCode != 0) { - throw new IOException("Command process failed with exit code " + exitCode + ". Error message: " + errorMessage); + throw new IOException( + "Command process failed with exit code " + + exitCode + + ". Error message: " + + errorMessage); } } } finally { @@ -110,26 +137,30 @@ public class ProcessExecutor { } return new ProcessExecutorResult(exitCode, messages); } - public class ProcessExecutorResult{ - int rc; - String messages; - public ProcessExecutorResult(int rc, String messages) { - this.rc = rc; - this.messages = messages; - } - public int getRc() { - return rc; - } - public void setRc(int rc) { - this.rc = rc; - } - public String getMessages() { - return messages; - } - public void setMessages(String messages) { - this.messages = messages; - } - - + + public class ProcessExecutorResult { + int rc; + String messages; + + public ProcessExecutorResult(int rc, String messages) { + this.rc = rc; + this.messages = messages; + } + + public int getRc() { + return rc; + } + + public void setRc(int rc) { + this.rc = rc; + } + + public String getMessages() { + return messages; + } + + public void setMessages(String messages) { + this.messages = messages; + } } } diff --git a/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java b/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java index 8d12267c..aa3e453d 100644 --- a/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java +++ b/src/main/java/stirling/software/SPDF/utils/PropertyConfigs.java @@ -4,46 +4,35 @@ import java.util.List; public class PropertyConfigs { - - public static boolean getBooleanValue(List keys, boolean defaultValue) { - for (String key : keys) { - String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); - - if (value != null) - return Boolean.valueOf(value); - } - return defaultValue; - } + public static boolean getBooleanValue(List keys, boolean defaultValue) { + for (String key : keys) { + String value = System.getProperty(key); + if (value == null) value = System.getenv(key); - public static String getStringValue(List keys, String defaultValue) { - for (String key : keys) { - String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); - - if (value != null) - return value; - } - return defaultValue; - } + if (value != null) return Boolean.valueOf(value); + } + return defaultValue; + } - - - - public static boolean getBooleanValue(String key, boolean defaultValue) { + public static String getStringValue(List keys, String defaultValue) { + for (String key : keys) { + String value = System.getProperty(key); + if (value == null) value = System.getenv(key); + + if (value != null) return value; + } + return defaultValue; + } + + public static boolean getBooleanValue(String key, boolean defaultValue) { String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); + if (value == null) value = System.getenv(key); return (value != null) ? Boolean.valueOf(value) : defaultValue; } - public static String getStringValue(String key, String defaultValue) { + public static String getStringValue(String key, String defaultValue) { String value = System.getProperty(key); - if (value == null) - value = System.getenv(key); + if (value == null) value = System.getenv(key); return (value != null) ? value : defaultValue; } - } diff --git a/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java new file mode 100644 index 00000000..b320f67e --- /dev/null +++ b/src/main/java/stirling/software/SPDF/utils/RequestUriUtils.java @@ -0,0 +1,14 @@ +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"); + } +} diff --git a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java index 131aaf03..4958a00d 100644 --- a/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/WebResponseUtils.java @@ -12,53 +12,56 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; - public class WebResponseUtils { - public static ResponseEntity boasToWebResponse(ByteArrayOutputStream baos, String docName) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); - } + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName); + } - public static ResponseEntity boasToWebResponse(ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { - return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); - } + public static ResponseEntity boasToWebResponse( + ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException { + return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType); + } + public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) + throws IOException { + String fileName = file.getOriginalFilename(); + MediaType mediaType = MediaType.parseMediaType(file.getContentType()); - public static ResponseEntity multiPartFileToWebResponse(MultipartFile file) throws IOException { - String fileName = file.getOriginalFilename(); - MediaType mediaType = MediaType.parseMediaType(file.getContentType()); + byte[] bytes = file.getBytes(); - byte[] bytes = file.getBytes(); - - return bytesToWebResponse(bytes, fileName, mediaType); - } + return bytesToWebResponse(bytes, fileName, mediaType); + } - public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName, MediaType mediaType) throws IOException { - - // Return the PDF as a response - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(mediaType); - headers.setContentLength(bytes.length); - String encodedDocName = URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20"); - headers.setContentDispositionFormData("attachment", encodedDocName); - return new ResponseEntity<>(bytes, headers, HttpStatus.OK); - } + public static ResponseEntity bytesToWebResponse( + byte[] bytes, String docName, MediaType mediaType) throws IOException { - public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) throws IOException { - return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); - } + // Return the PDF as a response + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + headers.setContentLength(bytes.length); + String encodedDocName = + URLEncoder.encode(docName, StandardCharsets.UTF_8.toString()) + .replaceAll("\\+", "%20"); + headers.setContentDispositionFormData("attachment", encodedDocName); + return new ResponseEntity<>(bytes, headers, HttpStatus.OK); + } - public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) throws IOException { - - // Open Byte Array and save document to it - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - document.save(baos); - // Close the document - document.close(); - - return boasToWebResponse(baos, docName); - } - + public static ResponseEntity bytesToWebResponse(byte[] bytes, String docName) + throws IOException { + return bytesToWebResponse(bytes, docName, MediaType.APPLICATION_PDF); + } + public static ResponseEntity pdfDocToWebResponse(PDDocument document, String docName) + throws IOException { + // Open Byte Array and save document to it + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + document.save(baos); + // Close the document + document.close(); + + return boasToWebResponse(baos, docName); + } } diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt new file mode 100644 index 00000000..e3188c2a --- /dev/null +++ b/src/main/resources/banner.txt @@ -0,0 +1,6 @@ + ____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____ +/ ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___| +\___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_ + ___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _| +|____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_| +Powered by Spring Boot ${spring-boot.version} \ No newline at end of file diff --git a/src/main/resources/messages_ar_AR.properties b/src/main/resources/messages_ar_AR.properties index 90643fad..61b98ebd 100644 --- a/src/main/resources/messages_ar_AR.properties +++ b/src/main/resources/messages_ar_AR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=إزالة الصفحات الفارغة home.removeBlanks.desc=يكتشف ويزيل الصفحات الفارغة من المستند 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.desc=يقارن ويظهر الاختلافات بين 2 من مستندات PDF compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=النسبة المئوية للصفحة التي removeBlanks.submit=إزالة الفراغات +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=يقارن compare.header=قارن ملفات PDF diff --git a/src/main/resources/messages_bg_BG.properties b/src/main/resources/messages_bg_BG.properties index f41dfe46..33052911 100644 --- a/src/main/resources/messages_bg_BG.properties +++ b/src/main/resources/messages_bg_BG.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Роля adminUserSettings.actions=Действия adminUserSettings.apiUser=Ограничен API потребител adminUserSettings.webOnlyUser=Само за уеб-потребител +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Принудете потребителя да промени потребителското име/парола при влизане adminUserSettings.submit=Съхранете потребителя @@ -255,6 +256,10 @@ home.removeBlanks.title=Премахване на празни страници home.removeBlanks.desc=Открива и премахва празни страници от документ 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.desc=Сравнява и показва разликите между 2 PDF документа compare.tags=разграничаване,контраст,промени,анализ @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Процент от страницата, коят removeBlanks.submit=Премахване на празни места +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Сравнявай compare.header=Сравнявай PDF-и diff --git a/src/main/resources/messages_ca_CA.properties b/src/main/resources/messages_ca_CA.properties index 02ec4c84..497a38ab 100644 --- a/src/main/resources/messages_ca_CA.properties +++ b/src/main/resources/messages_ca_CA.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Accions adminUserSettings.apiUser=Usuari amb API limitada adminUserSettings.webOnlyUser=Usuari només WEB +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login 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 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.desc=Compara i mostra les diferències entre 2 documents PDF 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Comparar compare.header=Compara PDF diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 30e8abf3..10e17038 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rolle adminUserSettings.actions=Aktion adminUserSettings.apiUser=Eingeschränkter API-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.submit=Benutzer speichern @@ -255,6 +256,10 @@ home.removeBlanks.title=Leere Seiten entfernen home.removeBlanks.desc=Erkennt und entfernt leere Seiten aus einem Dokument 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.desc=Vergleicht und zeigt die Unterschiede zwischen zwei PDF-Dokumenten an 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Vergleichen compare.header=PDFs vergleichen diff --git a/src/main/resources/messages_el_GR.properties b/src/main/resources/messages_el_GR.properties index a90e48f3..cb7fefa9 100644 --- a/src/main/resources/messages_el_GR.properties +++ b/src/main/resources/messages_el_GR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=\u03A1\u03CC\u03BB\u03BF\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.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.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 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.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 @@ -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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 compare.header=\u03A3\u03CD\u03B3\u03BA\u03C1\u03B9\u03C3\u03B7 PDFs diff --git a/src/main/resources/messages_en_GB.properties b/src/main/resources/messages_en_GB.properties index 3d06ab06..bfdc83cc 100644 --- a/src/main/resources/messages_en_GB.properties +++ b/src/main/resources/messages_en_GB.properties @@ -123,6 +123,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange = Force user to change username/password on login adminUserSettings.submit=Save User diff --git a/src/main/resources/messages_en_US.properties b/src/main/resources/messages_en_US.properties index 1a73c4d4..870b1ad7 100644 --- a/src/main/resources/messages_en_US.properties +++ b/src/main/resources/messages_en_US.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User diff --git a/src/main/resources/messages_es_ES.properties b/src/main/resources/messages_es_ES.properties index fa476586..4a520a50 100644 --- a/src/main/resources/messages_es_ES.properties +++ b/src/main/resources/messages_es_ES.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Acciones adminUserSettings.apiUser=Usuario limitado de API 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.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 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.desc=Comparar y mostrar las diferencias entre 2 documentos PDF 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 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 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 +#removeAnnotations +removeAnnotations.title=Eliminar anotaciones +removeAnnotations.header=Eliminar anotaciones +removeAnnotations.submit=Eliminar + + #compare compare.title=Comparar compare.header=Comparar archivos PDF @@ -861,7 +872,7 @@ split-by-size-or-count.submit=Enviar #overlay-pdfs 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.mode.label=Seleccione modo de superposición overlay-pdfs.mode.sequential=Superposición Sequencial diff --git a/src/main/resources/messages_eu_ES.properties b/src/main/resources/messages_eu_ES.properties index 69121553..642d7405 100644 --- a/src/main/resources/messages_eu_ES.properties +++ b/src/main/resources/messages_eu_ES.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Ekintzak adminUserSettings.apiUser=APIren erabiltzaile mugatua adminUserSettings.webOnlyUser=Web-erabiltzailea bakarrik +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Gorde Erabiltzailea @@ -255,6 +256,10 @@ home.removeBlanks.title=Ezabatu orrialde zuriak home.removeBlanks.desc=Detektatu orrialde zuriak eta dokumentutik ezabatu 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.desc=Konparatu eta erakutsi 2 PDF dokumenturen aldeak compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Zuria izan behar den orriaren ehunekoa ezabatua iz removeBlanks.submit=Ezabatu zuriuneak +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Konparatu compare.header=Konparatu PDF fitxategiak diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index b36ca85c..7969690c 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rôle adminUserSettings.actions=Actions adminUserSettings.apiUser=Utilisateur API limité 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.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. 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.desc=Comparez et visualisez les différences entre deux PDF. 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Comparer compare.header=Comparer diff --git a/src/main/resources/messages_hi_IN.properties b/src/main/resources/messages_hi_IN.properties index 7ffcf677..57988512 100644 --- a/src/main/resources/messages_hi_IN.properties +++ b/src/main/resources/messages_hi_IN.properties @@ -1,7 +1,7 @@ ########### # 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 pdfPrompt=पीडीएफ़(फ़ाइलें) चुनें @@ -92,7 +92,7 @@ account.title=खाता सेटिंग्स account.accountSettings=खाता सेटिंग्स account.adminSettings=व्यवस्थापक सेटिंग्स - उपयोगकर्ताओं को देखें और जोड़ें account.userControlSettings=उपयोगकर्ता नियंत्रण सेटिंग्स -account.changeUsername=नया उपयोगकर्ता नाम +account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें account.changeUsername=उपयोगकर्ता नाम परिवर्तन करें account.password=पासवर्ड पुष्टि account.oldPassword=पुराना पासवर्ड @@ -119,6 +119,7 @@ adminUserSettings.role=रोल adminUserSettings.actions=क्रियाएँ adminUserSettings.apiUser=सीमित API उपयोगकर्ता adminUserSettings.webOnlyUser=केवल वेब उपयोगकर्ता +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=उपयोगकर्ता को लॉगिन पर उपयोगकर्ता नाम/पासवर्ड बदलने के लिए मजबूर करें adminUserSettings.submit=उपयोगकर्ता को सहेजें @@ -175,24 +176,24 @@ home.permissions.title=अनुमतियाँ बदलें home.permissions.desc=अपने पीडीएफ़ दस्तावेज़ की अनुमतियाँ बदलें permissions.tags=पढ़ें, लिखें, संपादित करें, प्रिंट + home.removePages.title=हटाएं home.removePages.desc=अपने पीडीएफ़ दस्तावेज़ से अनचाहे पृष्ठों को हटाएं। removePages.tags=पृष्ठ हटाएं, पृष्ठ मिटाएं + home.addPassword.title=पासवर्ड जोड़ें home.addPassword.desc=अपने पीडीएफ़ दस्तावेज़ को एक पासवर्ड से एन्क्रिप्ट करें। addPassword.tags=सुरक्षित, सुरक्षा + home.removePassword.title=पासवर्ड हटाएं home.removePassword.desc=अपने पीडीएफ़ दस्तावेज़ से पासवर्ड सुरक्षा को हटाएं। 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 को कम्प्रेस करें। compressPdfs.tags=स्क्विश, छोटा, छोटा + home.changeMetadata.title=मेटाडेटा बदलें home.changeMetadata.desc=PDF दस्तावेज़ से मेटाडेटा बदलें/हटाएं/जोड़ें। changeMetadata.tags=शीर्षक, लेखक, तारीख, निर्माण, समय, प्रकाशक, उत्पादक, आँकड़े @@ -205,6 +206,7 @@ home.ocr.title=OCR / स्कैन को साफ करें home.ocr.desc=स्कैन को साफ करता है और पीडीएफ़ में छवियों से पाठ को पहचानता है और टेक्स्ट के रूप में फिर से जोड़ता है। ocr.tags=पहचान, टेक्स्ट, छवि, स्कैन, पढ़ें, पहचान, पता लगाना, संपादनीय + home.extractImages.title=छवियां निकालें home.extractImages.desc=पीडीएफ़ से सभी छवियों को निकालता है और उन्हें ज़िप में सहेजता है extractImages.tags=चित्र, फोटो, सहेजें, संग्रह, ज़िप, कैप्चर, ग्रैब @@ -213,7 +215,6 @@ home.pdfToPDFA.title=PDF से PDF/A में home.pdfToPDFA.desc=लंबे समय के लिए स्टोरेज के लिए पीडीएफ़ को पीडीएफ़/ए में रूपांतरित करें pdfToPDFA.tags=संग्रह, लंबे समय के लिए, मानक, परिवर्तन, स्टोरेज, संरक्षण - home.PDFToWord.title=PDF से वर्ड में home.PDFToWord.desc=PDF को वर्ड प्रारूपों में रूपांतरित करें (DOC, DOCX और ODT) PDFToWord.tags=doc,docx,odt,word,परिवर्तन,प्रारूप,रूपांतरण,ऑफिस,माइक्रोसॉफ्ट,डॉक फ़ाइल @@ -230,6 +231,7 @@ home.PDFToHTML.title=PDF से HTML में home.PDFToHTML.desc=PDF को HTML प्रारूप में रूपांतरित करें PDFToHTML.tags=वेब सामग्री, ब्राउज़र अनुकूल + home.PDFToXML.title=PDF से XML में home.PDFToXML.desc=PDF को XML प्रारूप में रूपांतरित करें PDFToXML.tags=डेटा-निकालन, संरचित सामग्री, अंतरसंवाद, परिवर्तन, रूपांतरण @@ -238,7 +240,6 @@ home.ScannerImageSplit.title=स्कैन की गई फोटो का home.ScannerImageSplit.desc=एक फोटो/PDF के भीतर से कई फोटो को विभाजित करता है ScannerImageSplit.tags=अलग, ऑटो-डिटेक्ट, स्कैन, मल्टी-फोटो, संगठित - home.sign.title=हस्ताक्षर home.sign.desc=हस्ताक्षर को ड्राइंग, पाठ या छवि के रूप में पीडीएफ़ में जोड़ता है। sign.tags=अधिकृत करें, आदेश, ड्राइंग-हस्ताक्षर, पाठ-हस्ताक्षर, छवि-हस्ताक्षर @@ -331,10 +332,10 @@ home.PdfToSinglePage.title=पीडीएफ़ से एक बड़े प home.PdfToSinglePage.desc=सभी पीडीएफ़ पेजों को एक बड़े एकल पृष्ठ में मर्ज करता है PdfToSinglePage.tags=एकल पृष्ठ + home.showJS.title=जावास्क्रिप्ट दिखाएं home.showJS.desc=पीडीएफ़ में डाला गया कोई भी जावास्क्रिप्ट खोजता है और प्रदर्शित करता है -showJS.tags=जेएस - +showJS.tags=गोपनीयकरण, छिपाना, काला करना, काला, मार्कर, छिपा हुआ home.autoRedact.title=स्वतः गोपनीयकरण home.autoRedact.desc=प्रविष्ट पाठ के आधार पर पीडीएफ़ में पाठ को स्वतः गोपनीयकरित(काला करें) @@ -462,9 +463,9 @@ addPageNumbers.submit=पृष्ठ संख्या जोड़ें #auto-rename -auto-rename.title=Auto Rename -auto-rename.header=Auto Rename PDF -auto-rename.submit=Auto Rename +auto-rename.title=स्वतः नाम परिवर्तन (खुद ब खुद नाम बदलें) +auto-rename.header=स्वतः नाम परिवर्तन पीडीएफ़ +auto-rename.submit=स्वतः नाम परिवर्तन #adjustContrast @@ -508,6 +509,7 @@ pageLayout.pagesPerSheet=प्रति पृष्ठ पेज: pageLayout.addBorder=सीमा जोड़ें pageLayout.submit=प्रस्तुत क + #scalePages scalePages.title=पृष्ठ-स्केल समायोजित करें scalePages.header=पृष्ठ-स्केल समायोजित करें @@ -516,7 +518,6 @@ scalePages.scaleFactor=पृष्ठ का ज़ूम स्तर (क् scalePages.submit=प्रस्तुत करें - #certSign certSign.title=प्रमाणपत्र साइनिंग certSign.header=अपने प्रमाणपत्र के साथ एक पीडीएफ़ पर हस्ताक्षर करें (काम जारी है) @@ -591,7 +592,7 @@ ScannerImageSplit.selectText.8=फोटो के लिए न्यूनत ScannerImageSplit.selectText.9=बॉर्डर का आकार: ScannerImageSplit.selectText.10=निकालने और जोड़ने के लिए जोड़ा जाने वाला बॉर्डर का आकार सेट करता है ताकि आउटपुट में सफेद बॉर्डर न आए (डिफ़ॉल्ट: 1)। - + #OCR ocr.title=OCR / स्कैन सफाई ocr.header=स्कैन सफाई / OCR (ऑप्टिकल कैरेक्टर रिकग्निशन) @@ -635,7 +636,7 @@ compress.selectText.1=मैनुअल मोड - 1 से 4 तक compress.selectText.2=अनुकूलन स्तर: compress.selectText.3=4 (पाठ छवियों के लिए अत्यधिक) compress.selectText.4=स्वत: मोड - निर्धारित आकार पाने के लिए गुणवत्ता को स्वत: समायोजित करता है -compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB) +compress.selectText.5=प्रत्याशित PDF आकार (जैसे 25MB, 10.8MB, 25KB) compress.submit=संकुचित करें @@ -710,8 +711,8 @@ imageToPDF.selectText.2=पीडीएफ को ऑटो रोटेट क imageToPDF.selectText.3=मल्टी फ़ाइल तर्क (केवल यदि कई छवियों के साथ काम किया जा रहा है) imageToPDF.selectText.4=एक ही पीडीएफ में मर्ज करें imageToPDF.selectText.5=अलग-अलग पीडीएफ में परिवर्तित करें - - + + #pdfToImage pdfToImage.title=पीडीएफ से छवि pdfToImage.header=पीडीएफ से छवि @@ -789,7 +790,7 @@ removePassword.submit=हटाएं #changeMetadata -changeMetadata.title=मेटाडेटा बदलें +changeMetadata.title=शीर्षक: changeMetadata.header=मेटाडेटा बदलें changeMetadata.selectText.1=कृपया उन चरों को संपादित करें जिन्हें आप बदलना चाहते हैं changeMetadata.selectText.2=सभी मेटाडेटा हटाएं diff --git a/src/main/resources/messages_hu_HU.properties b/src/main/resources/messages_hu_HU.properties index 0ae1324e..a4f47250 100644 --- a/src/main/resources/messages_hu_HU.properties +++ b/src/main/resources/messages_hu_HU.properties @@ -1,7 +1,7 @@ ########### # 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 pdfPrompt=Válasszon PDF-fájlokat @@ -19,7 +19,7 @@ save=Mentés close=Bezárás filesSelected=kiválasztott fájlok noFavourites=Nincs hozzáadva kedvenc -bored=Unatkozol? +bored=Unatkozol? alphabet=Ábécé downloadPdf=PDF letöltése text=Szöveg @@ -50,6 +50,7 @@ incorrectPasswordMessage=A jelenlegi jelszó helytelen. usernameExistsMessage=Az új felhasználónév már létezik. + ############# # NAVBAR # ############# @@ -60,7 +61,6 @@ navbar.darkmode=Sötét mód navbar.pageOps=Lap műveletek navbar.settings=Beállítások - ############# # SETTINGS # ############# @@ -76,6 +76,7 @@ settings.signOut=Kijelentkezés settings.accountSettings=Fiókbeállítások + changeCreds.title=Hitelesítés megváltoztatása 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é) @@ -86,11 +87,13 @@ changeCreds.confirmNewPassword=Új jelszó megerősítése changeCreds.submit=Változtatások elküldése + account.title=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.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.password=Megerősítő jelszó account.oldPassword=Régi jelszó account.newPassword=Új jelszó @@ -116,16 +119,17 @@ adminUserSettings.role=Szerep adminUserSettings.actions=Műveletek adminUserSettings.apiUser=Korlátozott API-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.submit=Felhasználó mentése - ############# # HOME-PAGE # ############# home.desc=Lokálisan hostolt egyszerű megoldás minden PDF igényéhez. home.searchBar=Keresés funkciókra... + home.viewPdf.title=PDF Megtekintése 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 @@ -140,12 +144,13 @@ merge.tags=egyesítés,Lapműveletek,Háttér,server oldal home.split.title=Osztás 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.desc=PDF-ek egyszerű forgatása. rotate.tags=server oldal + home.imageToPdf.title=Kép PDF-be home.imageToPdf.desc=Kép (PNG, JPEG, GIF) konvertálása PDF-fé. 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 pdfOrganiser.tags=duplex,páros,páratlan,rendezés,mozgatás + home.addImage.title=Kép hozzáadása home.addImage.desc=Kép hozzáadása a PDF megadott helyére 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 permissions.tags=olvasás,írás,szerkesztés,nyomtatás + home.removePages.title=Eltávolítás 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 @@ -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. compressPdfs.tags=szorít,kicsi,miniatűr + 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 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. 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.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 @@ -222,6 +231,7 @@ home.PDFToHTML.title=PDF >> HTML home.PDFToHTML.desc=PDF konvertálása HTML formátumra PDFToHTML.tags=web tartalom,böngészőbarát + home.PDFToXML.title=PDF >> XML home.PDFToXML.desc=PDF konvertálása XML formátumra 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 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.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 @@ -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 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 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 HTMLToPDF.tags=markup,web-tartalom,transzformáció,konverzió + home.MarkdownToPDF.title=Markdown PDF-be home.MarkdownToPDF.desc=Bármely Markdown fájl átalakítása PDF-be MarkdownToPDF.tags=markup,web-tartalom,transzformáció,konverzió + 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 getPdfInfo.tags=információ,adat,statisztika,statisztika + home.extractPage.title=Lapok kinyerése home.extractPage.desc=Válassza ki a lapokat a PDF-ből extractPage.tags=kinyer + home.PdfToSinglePage.title=PDF egyetlen nagy lapba home.PdfToSinglePage.desc=Az összes PDF lap egyesítése egyetlen nagy lapba PdfToSinglePage.tags=egyetlen lap + home.showJS.title=JavaScript megjelenítése 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.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 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.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 + 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 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 split-by-sections.tags=Szakasz elosztás, felosztás, testreszabás - ########################### # # # WEB PAGES # @@ -400,6 +420,7 @@ MarkdownToPDF.help=Az átalakítás folyamatban MarkdownToPDF.credit=WeasyPrint alkalmazása + #url-to-pdf URLToPDF.title=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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Összehasonlítás 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.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.title=OCR / szkennelés tisztázása 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.header=PDF többfunkciós eszköz - #view pdf viewPdf.title=PDF megtekintése viewPdf.header=PDF megtekintése - #pageRemover pageRemover.title=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.4=Egyesítse egyetlen PDF-fé imageToPDF.selectText.5=Átalakítás különálló PDF-fé - - + + #pdfToImage pdfToImage.title=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.title=Metaadatok módosítása +changeMetadata.title=Cím: 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.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.submit=Konvertálás - #PDFToCSV PDFToCSV.title=PDF >> CSV PDFToCSV.header=PDF >> CSV PDFToCSV.prompt=Válassza ki az oldalt a táblázat kinyeréséhez PDFToCSV.submit=Kinyerés - #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.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.vertical.placeholder=Adja meg a függőleges szakaszok számát split-by-sections.submit=Felosztás - diff --git a/src/main/resources/messages_id_ID.properties b/src/main/resources/messages_id_ID.properties index 7c01ced1..8a00dd9f 100644 --- a/src/main/resources/messages_id_ID.properties +++ b/src/main/resources/messages_id_ID.properties @@ -25,7 +25,7 @@ downloadPdf=Unduh PDF text=Teks font=Jenis huruf selectFillter=-- Pilih -- -pageNum=Nomor Halaman +pageNum=Nomor Halaman sizes.small=Kecil sizes.medium=Sedang sizes.large=Besar @@ -92,7 +92,7 @@ account.title=Pengaturan Akun account.accountSettings=Pengaturan Akun account.adminSettings=Pengaturan Admin - Melihat dan Menambahkan Pengguna account.userControlSettings=Pengaturan Kontrol Pengguna -account.changeUsername=Nama Pengguna Baru +account.changeUsername=Ubah Nama Pengguna account.changeUsername=Ubah Nama Pengguna account.password=Konfirmasi Kata sandi account.oldPassword=Kata sandi lama @@ -119,6 +119,7 @@ adminUserSettings.role=Peran adminUserSettings.actions=Tindakan adminUserSettings.apiUser=Pengguna API Terbatas 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.submit=Simpan Pengguna @@ -143,7 +144,7 @@ merge.tags=menggabungkan,Pengoperasian halaman,Back end,sisi server home.split.title=Membagi 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.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 PdfToSinglePage.tags=halaman tunggal + home.showJS.title=Tampilkan Javascript 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.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.downloadJson=Unduh JSON + #markdown-to-pdf MarkdownToPDF.title=Markdown ke PDF MarkdownToPDF.header=Markdown Ke PDF @@ -417,6 +420,7 @@ MarkdownToPDF.help=Pekerjaan sedang berlangsung MarkdownToPDF.credit=Menggunakan WeasyPrint + #url-to-pdf URLToPDF.title=URL ke PDF URLToPDF.header=URL Ke PDF @@ -517,22 +521,22 @@ scalePages.submit=Kirim #certSign certSign.title=Penandatanganan Sertifikat certSign.header=Menandatangani PDF dengan sertifikat Anda (Sedang dalam proses) -certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan: -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.selectP12=Pilih Berkas Keystore PKCS #12 Anda (.p12 atau .pfx) (Opsional, Jika disediakan, berkas tersebut harus berisi kunci pribadi dan sertifikat Anda): +certSign.selectPDF=Pilih Berkas PDF untuk Penandatanganan: +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.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.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.reason=Alasan certSign.location=Lokasi -certSign.name=Nama +certSign.name=Nama certSign.submit=Tanda tangani PDF #removeBlanks removeBlanks.title=Hapus Halaman Kosong -hapusKosong.header=Hapus Halaman Kosong +removeBlanks.header=Remove Blank Pages 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.whitePercent=Persen Putih (%): @@ -553,6 +557,7 @@ compare.document.1=Dokumen 1 compare.document.2=Dokumen 2 compare.submit=Bandingkan + #sign sign.title=Tanda 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.submit=Memproses PDF dengan OCR + #extractImages extractImages.title=Ekstrak Gambar extractImages.header=Mengekstrak Gambar @@ -616,7 +622,7 @@ extractImages.submit=Ekstrak #File to 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.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 @@ -630,7 +636,7 @@ compress.selectText.1=Mode Manual - Dari 1 hingga 4 compress.selectText.2=Tingkat Optimalisasi: 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.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 @@ -750,7 +756,7 @@ watermark.selectText.1=Pilih PDF untuk menambahkan watermark: watermark.selectText.2=Text Watermark: watermark.selectText.3=Ukuran Huruf: 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.7=Opacity (0% - 100%): watermark.selectText.8=Tipe Watermark: @@ -784,7 +790,7 @@ removePassword.submit=Hapus #changeMetadata -changeMetadata.title=Ganti Metadata +changeMetadata.title=Judul: changeMetadata.header=Ganti Metadata changeMetadata.selectText.1=Silakan edit variabel yang ingin Anda ubah changeMetadata.selectText.2=Hapus semua metadata @@ -855,7 +861,7 @@ PDFToCSV.submit=Ektraksi #split-by-size-or-count 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.pageCount=Berdasarkan Jumlah Halaman split-by-size-or-count.type.docCount=Berdasarkan Jumlah Dokumen diff --git a/src/main/resources/messages_it_IT.properties b/src/main/resources/messages_it_IT.properties index 52f106a3..8801f014 100644 --- a/src/main/resources/messages_it_IT.properties +++ b/src/main/resources/messages_it_IT.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Ruolo adminUserSettings.actions=Azioni adminUserSettings.apiUser=Utente API limitato adminUserSettings.webOnlyUser=Utente solo Web +adminUserSettings.demoUser=Utente demo (nessuna impostazione personalizzata) adminUserSettings.forceChange=Forza l'utente a cambiare nome username/password all'accesso adminUserSettings.submit=Salva utente @@ -164,8 +165,8 @@ pdfOrganiser.tags=duplex,pari,dispari,ordinamento,spostamento home.addImage.title=Aggiungi Immagine -home.addImage.desc=Aggiungi un'immagine in un punto specifico del PDF (Work in progress) -addImage.tags=img,jpg,picture,photo +home.addImage.desc=Aggiungi un'immagine in un punto specifico del PDF (Lavori in corso) +addImage.tags=img,jpg,immagine,photo home.watermark.title=Aggiungi Filigrana home.watermark.desc=Aggiungi una filigrana al tuo PDF. @@ -241,7 +242,7 @@ ScannerImageSplit.tags=separa,rileva automaticamente,scansiona,multi-foto,organi home.sign.title=Firma home.sign.desc=Aggiungi una firma al PDF da disegno, testo o immagine. -sign.tags=autorizza,iniziali,firma-tracciata,segno-testo,firma-immagine +sign.tags=autorizza,iniziali,firma-tracciata,firma-testo,firma-immagine home.flatten.title=Appiattisci home.flatten.desc=Rimuovi tutti gli elementi interattivi e moduli da un PDF. @@ -255,6 +256,10 @@ home.removeBlanks.title=Rimuovi pagine vuote home.removeBlanks.desc=Trova e rimuovi pagine vuote da un PDF. removeBlanks.tags=pulire,semplificare,non contenere contenuti,organizzare +home.removeAnnotations.title=Rimuovi annotazioni +home.removeAnnotations.desc=Rimuove tutti i commenti/annotazioni da un PDF +removeAnnotations.tags=commenti,evidenziazioni,note,markup,rimozione + home.compare.title=Compara home.compare.desc=Vedi e compara le differenze tra due PDF. compare.tags=differenziare,contrastare,cambiare,analisi @@ -438,7 +443,7 @@ sanitizePDF.selectText.1=Rimuovi le azioni JavaScript sanitizePDF.selectText.2=Rimuovi i file incorporati sanitizePDF.selectText.3=Rimuovi i metadati sanitizePDF.selectText.4=Rimuovi collegamenti -sanitizePDF.selectText.5=Rimuovi i fonts +sanitizePDF.selectText.5=Rimuovi i font sanitizePDF.submit=Pulisci PDF @@ -452,7 +457,7 @@ addPageNumbers.selectText.4=Numero di partenza addPageNumbers.selectText.5=Pagine da numerare addPageNumbers.selectText.6=Testo personalizzato addPageNumbers.customTextDesc=Testo personalizzato -addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "all", accetta anche 1-5 o 2,5,9 ecc +addPageNumbers.numberPagesDesc=Quali pagine numerare, impostazione predefinita "tutte", accetta anche 1-5 o 2,5,9 ecc addPageNumbers.customNumberDesc=Il valore predefinito è {n}, accetta anche 'Pagina {n} di {total}', 'Testo-{n}', '{filename}-{n} addPageNumbers.submit=Aggiungi numeri di pagina @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Percentuale della pagina che deve essere bianca pe removeBlanks.submit=Rimuovi +#removeAnnotations +removeAnnotations.title=Rimuovi Annotazioni +removeAnnotations.header=Remuovi Annotazioni +removeAnnotations.submit=Rimuovi + + #compare compare.title=Compara compare.header=Compara PDF @@ -579,7 +590,7 @@ ScannerImageSplit.selectText.6=Imposta l'area minima di una foto (default: 10000 ScannerImageSplit.selectText.7=Area di contorno minima: ScannerImageSplit.selectText.8=Imposta l'area minima del contorno di una foto ScannerImageSplit.selectText.9=Spessore bordo: -ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (default: 1). +ScannerImageSplit.selectText.10=Imposta lo spessore del bordo aggiunto o rimosso per prevenire bordi bianchi nel risultato (predefinito: 1). #OCR @@ -783,7 +794,7 @@ changeMetadata.title=Titolo: changeMetadata.header=Cambia Proprietà changeMetadata.selectText.1=Imposta i dati che vuoi cambiare changeMetadata.selectText.2=Cancella tutte le proprietà -changeMetadata.selectText.3=Visualizza proprietà custom: +changeMetadata.selectText.3=Visualizza proprietà personalizzate: changeMetadata.author=Autore: changeMetadata.creationDate=Data di creazione (yyyy/MM/dd HH:mm:ss): changeMetadata.creator=Creatore: @@ -846,7 +857,7 @@ PDFToXML.submit=Converti PDFToCSV.title=Da PDF a CSV PDFToCSV.header=Da PDF a CSV PDFToCSV.prompt=Scegli la pagina per estrarre la tabella -PDFToCSV.submit=Estratto +PDFToCSV.submit=Estrai #split-by-size-or-count split-by-size-or-count.header=Dividi il PDF per dimensione o numero @@ -865,9 +876,9 @@ overlay-pdfs.baseFile.label=Seleziona File PDF di base overlay-pdfs.overlayFiles.label=Seleziona sovrapposizione file PDF overlay-pdfs.mode.label=Seleziona la modalità di sovrapposizione overlay-pdfs.mode.sequential=Sovrapposizione sequenziale -overlay-pdfs.mode.interleaved=Interleaved Overlay -overlay-pdfs.mode.fixedRepeat=Fixed Repeat Overlay -overlay-pdfs.counts.label=Overlay Counts (for Fixed Repeat Mode) +overlay-pdfs.mode.interleaved=Sovrapposizione interfogliata +overlay-pdfs.mode.fixedRepeat=Risolto il problema con la ripetizione della sovrapposizione +overlay-pdfs.counts.label=Numeri sovrapposti (per la modalità di ripetizione fissa) overlay-pdfs.counts.placeholder=Inserisci i numeri separati da virgole (ad esempio, 2,3,1) overlay-pdfs.position.label=Seleziona posizione di sovrapposizione overlay-pdfs.position.foreground=Primo piano diff --git a/src/main/resources/messages_ja_JP.properties b/src/main/resources/messages_ja_JP.properties index 04ad2da4..e4cb1ff6 100644 --- a/src/main/resources/messages_ja_JP.properties +++ b/src/main/resources/messages_ja_JP.properties @@ -119,6 +119,7 @@ adminUserSettings.role=役割 adminUserSettings.actions=アクション adminUserSettings.apiUser=限定されたAPIユーザー adminUserSettings.webOnlyUser=ウェブ専用ユーザー +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=ログイン時にユーザー名/パスワードを強制的に変更する adminUserSettings.submit=ユーザーの保存 @@ -255,6 +256,10 @@ home.removeBlanks.title=空白ページの削除 home.removeBlanks.desc=ドキュメントから空白ページを検出して削除します。 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.desc=2つのPDFを比較して表示します。 compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=削除するページの白の割合 removeBlanks.submit=空白ページの削除 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=比較 compare.header=PDFの比較 diff --git a/src/main/resources/messages_ko_KR.properties b/src/main/resources/messages_ko_KR.properties index 429575c4..01f15e66 100644 --- a/src/main/resources/messages_ko_KR.properties +++ b/src/main/resources/messages_ko_KR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=역할 adminUserSettings.actions=동작 adminUserSettings.apiUser=제한된 API 사용 adminUserSettings.webOnlyUser=웹 사용만 허용 +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=다음 로그인 때 사용자명과 비밀번호를 변경하도록 강제 adminUserSettings.submit=사용자 저장 @@ -255,6 +256,10 @@ home.removeBlanks.title=빈 페이지 제거 home.removeBlanks.desc=PDF 문서에서 빈 페이지를 감지하고 제거합니다. 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.desc=2개의 PDF 문서를 비교하고 차이를 표시합니다. compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=제거될 페이지의 흰색 픽셀 비율 removeBlanks.submit=빈 페이지 제거 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=비교 compare.header=PDF 문서 비교 diff --git a/src/main/resources/messages_nl_NL.properties b/src/main/resources/messages_nl_NL.properties index 262286bd..17087c07 100644 --- a/src/main/resources/messages_nl_NL.properties +++ b/src/main/resources/messages_nl_NL.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Acties adminUserSettings.apiUser=Beperkte API gebruiker adminUserSettings.webOnlyUser=Alleen web gebruiker +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login 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 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.desc=Vergelijkt en toont de verschillen tussen 2 PDF-documenten 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Vergelijken compare.header=PDF''s vergelijken diff --git a/src/main/resources/messages_pl_PL.properties b/src/main/resources/messages_pl_PL.properties index 4ba1c66f..1a725f65 100644 --- a/src/main/resources/messages_pl_PL.properties +++ b/src/main/resources/messages_pl_PL.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login 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 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.desc=Porównuje i pokazuje różnice między dwoma dokumentami PDF 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Porównaj compare.header=Porównaj PDF(y) diff --git a/src/main/resources/messages_ru_RU.properties b/src/main/resources/messages_ru_RU.properties index 17d16dcb..c29c8f24 100644 --- a/src/main/resources/messages_ru_RU.properties +++ b/src/main/resources/messages_ru_RU.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login adminUserSettings.submit=Save User @@ -255,6 +256,10 @@ home.removeBlanks.title=Удалить пустые страницы home.removeBlanks.desc=Обнаруживает и удаляет пустые страницы из документа 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.desc=Сравнивает и показывает различия между двумя PDF-документами compare.tags=differentiate,contrast,changes,analysis @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Общий процент белого на стр removeBlanks.submit=Удалить Пустые +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Сравнение compare.header=Сравнение PDFы diff --git a/src/main/resources/messages_sv_SE.properties b/src/main/resources/messages_sv_SE.properties index 780a3fb1..d090ef8e 100644 --- a/src/main/resources/messages_sv_SE.properties +++ b/src/main/resources/messages_sv_SE.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Role adminUserSettings.actions=Actions adminUserSettings.apiUser=Limited API User adminUserSettings.webOnlyUser=Web Only User +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=Force user to change username/password on login 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 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.desc=Jämför och visar skillnaderna mellan 2 PDF-dokument 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 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Jämför compare.header=Jämför PDF-filer diff --git a/src/main/resources/messages_tr_TR.properties b/src/main/resources/messages_tr_TR.properties index 098f491d..43eab312 100644 --- a/src/main/resources/messages_tr_TR.properties +++ b/src/main/resources/messages_tr_TR.properties @@ -119,6 +119,7 @@ adminUserSettings.role=Rol adminUserSettings.actions=Eylemler adminUserSettings.apiUser=Sınırlı API 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.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 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.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 @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=Bir sayfanın 'beyaz' pixel olması gereken yüzde removeBlanks.submit=Boşları Kaldır +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=Karşılaştır compare.header=PDF'leri Karşılaştır diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties index f5ec2339..808fac2c 100644 --- a/src/main/resources/messages_zh_CN.properties +++ b/src/main/resources/messages_zh_CN.properties @@ -119,6 +119,7 @@ adminUserSettings.role=角色 adminUserSettings.actions=操作 adminUserSettings.apiUser=有限 API 用户 adminUserSettings.webOnlyUser=仅限 Web 用户 +adminUserSettings.demoUser=Demo User (No custom settings) adminUserSettings.forceChange=强制用户在登录时更改用户名/密码 adminUserSettings.submit=保存用户 @@ -126,12 +127,12 @@ adminUserSettings.submit=保存用户 # HOME-PAGE # ############# home.desc=CZL一站式服务,满足您的所有PDF需求。 -home.searchBar=Search for features... +home.searchBar=搜索您需要的功能... -home.viewPdf.title=View PDF -home.viewPdf.desc=View, annotate, add text or images -viewPdf.tags=view,read,annotate,text,image +home.viewPdf.title=浏览PDF +home.viewPdf.desc=浏览、注释、添加文本或图像 +viewPdf.tags=浏览、阅读、注释、文本、图像 home.multiTool.title=PDF多功能工具 home.multiTool.desc=合并、旋转、重新排列和删除PDF页面 @@ -255,6 +256,10 @@ home.removeBlanks.title=删除空白页 home.removeBlanks.desc=检测并删除文档中的空白页 removeBlanks.tags=清理、简化、非内容、整理 +home.removeAnnotations.title=删除标注 +home.removeAnnotations.desc=删除PDF中的所有标注/评论 +removeAnnotations.tags=评论、高亮、笔记、标注、删除 + home.compare.title=比较 home.compare.desc=比较并显示两个PDF文档之间的差异 compare.tags=区分、对比、更改、分析 @@ -337,22 +342,22 @@ home.autoRedact.desc=根据输入文本自动删除(覆盖)PDF中的文本 showJS.tags=JavaScript home.tableExtraxt.title=PDF to CSV -home.tableExtraxt.desc=Extracts Tables from a PDF converting it to CSV -tableExtraxt.tags=CSV,Table Extraction,extract,convert +home.tableExtraxt.desc=从PDF中提取表格并将其转换为CSV +tableExtraxt.tags=CSV、表格提取、提取、转换 -home.autoSizeSplitPDF.title=Auto Split by Size/Count -home.autoSizeSplitPDF.desc=Split a single PDF into multiple documents based on size, page count, or document count -autoSizeSplitPDF.tags=pdf,split,document,organization +home.autoSizeSplitPDF.title=自动根据大小/数目拆分PDF +home.autoSizeSplitPDF.desc=将单个PDF拆分为多个文档,基于大小、页数或文档数 +autoSizeSplitPDF.tags=pdf、拆分、文件、组织 -home.overlay-pdfs.title=Overlay PDFs -home.overlay-pdfs.desc=Overlays PDFs on-top of another PDF -overlay-pdfs.tags=Overlay +home.overlay-pdfs.title=叠加PDF +home.overlay-pdfs.desc=将PDF叠加在另一个PDF上 +overlay-pdfs.tags=叠加 -home.split-by-sections.title=Split PDF by Sections -home.split-by-sections.desc=Divide each page of a PDF into smaller horizontal and vertical sections -split-by-sections.tags=Section Split, Divide, Customize +home.split-by-sections.title=拆分PDF成小块 +home.split-by-sections.desc=将PDF的每一页分割成更小的水平和垂直的部分 +split-by-sections.tags=章节拆分、分割、自定义 ########################### # # @@ -539,6 +544,12 @@ removeBlanks.whitePercentDesc=必须为白色才能删除的页面百分比 removeBlanks.submit=删除空白 +#removeAnnotations +removeAnnotations.title=Remove Annotations +removeAnnotations.header=Remove Annotations +removeAnnotations.submit=Remove + + #compare compare.title=比较 compare.header=比较 PDF @@ -843,43 +854,43 @@ PDFToXML.credit=此服务使用LibreOffice进行文件转换。 PDFToXML.submit=转换 #PDFToCSV -PDFToCSV.title=PDF ? CSV -PDFToCSV.header=PDF ? CSV -PDFToCSV.prompt=Choose page to extract table -PDFToCSV.submit=?? +PDFToCSV.title=PDF To CSV +PDFToCSV.header=将 PDF 转换为 CSV +PDFToCSV.prompt=选择需要提取表格的页面 +PDFToCSV.submit=提取 #split-by-size-or-count -split-by-size-or-count.header=Split PDF by Size or Count -split-by-size-or-count.type.label=Select Split Type -split-by-size-or-count.type.size=By Size -split-by-size-or-count.type.pageCount=By Page Count -split-by-size-or-count.type.docCount=By Document Count -split-by-size-or-count.value.label=Enter Value -split-by-size-or-count.value.placeholder=Enter size (e.g., 2MB or 3KB) or count (e.g., 5) -split-by-size-or-count.submit=Submit +split-by-size-or-count.header=按照大小或数目拆分PDF +split-by-size-or-count.type.label=选择拆分类型 +split-by-size-or-count.type.size=按照大小 +split-by-size-or-count.type.pageCount=按照页数 +split-by-size-or-count.type.docCount=按照文档数 +split-by-size-or-count.value.label=输入数值 +split-by-size-or-count.value.placeholder=输入大小(例如,2MB或3KB)或数目(例如,5) +split-by-size-or-count.submit=提交 #overlay-pdfs -overlay-pdfs.header=Overlay PDF Files -overlay-pdfs.baseFile.label=Select Base PDF File -overlay-pdfs.overlayFiles.label=Select Overlay PDF Files -overlay-pdfs.mode.label=Select Overlay Mode -overlay-pdfs.mode.sequential=Sequential Overlay -overlay-pdfs.mode.interleaved=Interleaved Overlay -overlay-pdfs.mode.fixedRepeat=Fixed Repeat Overlay -overlay-pdfs.counts.label=Overlay Counts (for Fixed Repeat Mode) -overlay-pdfs.counts.placeholder=Enter comma-separated counts (e.g., 2,3,1) -overlay-pdfs.position.label=Select Overlay Position -overlay-pdfs.position.foreground=Foreground -overlay-pdfs.position.background=Background -overlay-pdfs.submit=Submit +overlay-pdfs.header=叠加PDF文件 +overlay-pdfs.baseFile.label=选择基础PDF文件 +overlay-pdfs.overlayFiles.label=选择需要叠加在基础上的PDF文件 +overlay-pdfs.mode.label=选择叠加模式 +overlay-pdfs.mode.sequential=按顺序叠加 +overlay-pdfs.mode.interleaved=交错叠加 +overlay-pdfs.mode.fixedRepeat=固定重复叠加 +overlay-pdfs.counts.label=叠加次数(仅限固定重复叠加模式) +overlay-pdfs.counts.placeholder=输入用逗号分隔的次数(例如,2,3,1) +overlay-pdfs.position.label=选择叠加位置 +overlay-pdfs.position.foreground=前面(上面) +overlay-pdfs.position.background=后面(下面) +overlay-pdfs.submit=提交 #split-by-sections -split-by-sections.title=Split PDF by Sections -split-by-sections.header=Split PDF into Sections -split-by-sections.horizontal.label=Horizontal Divisions -split-by-sections.vertical.label=Vertical Divisions -split-by-sections.horizontal.placeholder=Enter number of horizontal divisions -split-by-sections.vertical.placeholder=Enter number of vertical divisions -split-by-sections.submit=Split PDF +split-by-sections.title=按照块(Section)拆分PDF +split-by-sections.header=将PDF拆分成块 +split-by-sections.horizontal.label=水平分割 +split-by-sections.vertical.label=垂直分割 +split-by-sections.horizontal.placeholder=输入水平分割数 +split-by-sections.vertical.placeholder=输入垂直分割数 +split-by-sections.submit=分割PDF diff --git a/src/main/resources/settings.yml.template b/src/main/resources/settings.yml.template index 0f229ba4..52d5e4de 100644 --- a/src/main/resources/settings.yml.template +++ b/src/main/resources/settings.yml.template @@ -5,10 +5,13 @@ security: enableLogin: false # set to 'true' to enable login csrfDisabled: true - + loginAttemptCount: 5 # lock user account after 5 tries + loginResetTimeMinutes : 120 # lock account for 2 hours after x attempts + system: 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 + enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes) #ui: # appName: exampleAppName # Application's visible name diff --git a/src/main/resources/static/css/dark-mode.css b/src/main/resources/static/css/dark-mode.css index 82e01676..2f070c9c 100644 --- a/src/main/resources/static/css/dark-mode.css +++ b/src/main/resources/static/css/dark-mode.css @@ -75,6 +75,13 @@ table th, table td { border: none; color: #fff !important; } + +.btn-warning { + background-color: #ffc107 !important; + border: none; + color: #000 !important; +} + .btn-outline-secondary { color: #fff !important; 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


*/ } +.modal-content { + color: #fff !important; + border-color: #fff; +} + #global-buttons-container input { background-color: #323948; caret-color: #ffffff; diff --git a/src/main/resources/static/css/home.css b/src/main/resources/static/css/home.css index ab3b3348..fe184637 100644 --- a/src/main/resources/static/css/home.css +++ b/src/main/resources/static/css/home.css @@ -1,5 +1,5 @@ #searchBar { - background-image: url('/images/search.svg'); + background-image: url('../images/search.svg'); background-position: 16px 16px; background-repeat: no-repeat; width: 100%; diff --git a/src/main/resources/static/js/merge.js b/src/main/resources/static/js/merge.js index d27730b9..b90c7be2 100644 --- a/src/main/resources/static/js/merge.js +++ b/src/main/resources/static/js/merge.js @@ -10,7 +10,6 @@ document.getElementById("fileInput-input").addEventListener("change", function() function displayFiles(files) { var list = document.getElementById("selectedFiles"); - list.innerHTML = ""; for (var i = 0; i < files.length; i++) { var item = document.createElement("li"); @@ -21,6 +20,7 @@ function displayFiles(files) {
+
`; @@ -56,6 +56,16 @@ function attachMoveButtons() { } }); } + + var removeButtons = document.querySelectorAll(".remove-file"); + for (var i = 0; i < removeButtons.length; i++) { + removeButtons[i].addEventListener("click", function (event) { + event.preventDefault(); + var parent = this.closest(".list-group-item"); + parent.remove(); + updateFiles(); + }); + } } document.getElementById("sortByNameBtn").addEventListener("click", function() { diff --git a/src/main/resources/static/js/pipeline.js b/src/main/resources/static/js/pipeline.js index 06810743..4fcde3a0 100644 --- a/src/main/resources/static/js/pipeline.js +++ b/src/main/resources/static/js/pipeline.js @@ -12,20 +12,17 @@ function validatePipeline() { if (currentOperation === '/add-password') { containsAddPassword = true; } - console.log(currentOperation); - console.log(apiDocs[currentOperation]); + let currentOperationDescription = apiDocs[currentOperation]?.post?.description || ""; let nextOperationDescription = apiDocs[nextOperation]?.post?.description || ""; - console.log("currentOperationDescription", currentOperationDescription); - console.log("nextOperationDescription", nextOperationDescription); - + // Strip off 'ZIP-' prefix + currentOperationDescription = currentOperationDescription.replace("ZIP-", ''); + nextOperationDescription = nextOperationDescription.replace("ZIP-", ''); + let currentOperationOutput = currentOperationDescription.match(/Output:([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 let currentOperationOutputArr = currentOperationOutput.split('/'); let nextOperationInputArr = nextOperationInput.split('/'); @@ -35,6 +32,7 @@ function validatePipeline() { console.log(`Intersection: ${intersection}`); if (intersection.length === 0) { + updateValidateButton(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}).`); 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') { + updateValidateButton(false); alert('The "add-password" operation should be at the end of the operations sequence. Please adjust the operations order.'); return false; } @@ -53,10 +52,20 @@ function validatePipeline() { console.error('Pipeline is not valid'); // Stop operation, maybe display an error to the user } - + updateValidateButton(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; } let selectedOperation = document.getElementById('operationsDropdown').value; - let parameters = operationSettings[selectedOperation] || {}; + + + var pipelineName = document.getElementById('pipelineName').value; + let pipelineList = document.getElementById('pipelineList').children; let pipelineConfig = { - "name": "uniquePipelineName", - "pipeline": [{ - "operation": selectedOperation, - "parameters": parameters - }], + "name": pipelineName, + "pipeline": [], "_examples": { "outputDir": "{outputFolder}/{folderName}", "outputFileName": "{filename}-{pipelineName}-{date}-{time}" @@ -83,6 +92,28 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() "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 formData = new FormData(); @@ -99,37 +130,50 @@ document.getElementById('submitConfigBtn').addEventListener('click', function() formData.append('json', pipelineConfigJson); console.log("formData", formData); - fetch('/handleData', { - method: 'POST', - body: formData + fetch('api/v1/pipeline/handleData', { + method: 'POST', + body: formData }) - .then(response => response.blob()) - .then(blob => { + .then(response => { + // 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 apiSchemas = {}; let operationSettings = {}; -fetch('v3/api-docs') +fetch('v1/api-docs') .then(response => response.json()) .then(data => { apiDocs = data.paths; + apiSchemas = data.components.schemas; 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 = ''; @@ -138,6 +182,9 @@ fetch('v3/api-docs') // Group operations by tags Object.keys(data.paths).forEach(operationPath => { let operation = data.paths[operationPath].post; + if(!operation || !operation.description) { + console.log(operationPath); + } if (operation && !ignoreOperations.includes(operationPath) && !operation.description.includes("Type:MISO")) { let operationTag = operation.tags[0]; // This assumes each operation has exactly one tag if (!operationsByTag[operationTag]) { @@ -146,9 +193,8 @@ fetch('v3/api-docs') operationsByTag[operationTag].push(operationPath); } }); - // Specify the order of tags - let tagOrder = ["General", "Security", "Convert", "Other", "Filter"]; + let tagOrder = ["General", "Security", "Convert", "Misc", "Filter"]; // Create dropdown options tagOrder.forEach(tag => { @@ -158,8 +204,18 @@ fetch('v3/api-docs') operationsByTag[tag].forEach(operationPath => { 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 group.appendChild(option); }); @@ -176,25 +232,40 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let listItem = document.createElement('li'); listItem.className = "list-group-item"; - let hasSettings = (apiDocs[selectedOperation] && apiDocs[selectedOperation].post && - ((apiDocs[selectedOperation].post.parameters && apiDocs[selectedOperation].post.parameters.length > 0) || - (apiDocs[selectedOperation].post.requestBody && - apiDocs[selectedOperation].post.requestBody.content['multipart/form-data'].schema.properties))); + let hasSettings = false; + if (apiDocs[selectedOperation] && apiDocs[selectedOperation].post) { + const postMethod = apiDocs[selectedOperation].post; + + // 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 = ` -
-
${selectedOperation}
-
- - - - -
-
- `; +
+
${selectedOperation}
+
+ + + + +
+
+`; + pipelineList.appendChild(listItem); @@ -215,23 +286,28 @@ document.getElementById('addOperationBtn').addEventListener('click', function() listItem.querySelector('.remove').addEventListener('click', function(event) { event.preventDefault(); pipelineList.removeChild(listItem); + hideOrShowPipelineHeader(); }); listItem.querySelector('.pipelineSettings').addEventListener('click', function(event) { event.preventDefault(); showpipelineSettingsModal(selectedOperation); + hideOrShowPipelineHeader(); }); function showpipelineSettingsModal(operation) { let pipelineSettingsModal = document.getElementById('pipelineSettingsModal'); let pipelineSettingsContent = document.getElementById('pipelineSettingsContent'); 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 operationData = operationData.concat(Object.keys(requestBodyData).map(key => ({ - name: key, - schema: requestBodyData[key] + name: key, + schema: requestBodyData[key] }))); pipelineSettingsContent.innerHTML = ''; @@ -245,11 +321,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() let parameterLabel = document.createElement('label'); parameterLabel.textContent = `${parameter.name} (${parameter.schema.type}): `; - parameterLabel.title = parameter.description; + parameterLabel.title = parameter.schema.description; + parameterLabel.setAttribute('for', parameter.name); parameterDiv.appendChild(parameterLabel); + + let defaultValue = parameter.schema.example; + if (defaultValue === undefined) defaultValue = parameter.schema.default; let parameterInput; - + // check if enum exists in schema if (parameter.schema.enum) { // if enum exists, create a select element @@ -277,11 +357,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; - parameterInput.value = "automatedFileInput"; + parameterInput.value = "FileInputPathToBeInputtedManuallyOffline"; } else { parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; } break; case 'number': @@ -289,10 +370,12 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'number'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; break; case 'boolean': parameterInput = document.createElement('input'); parameterInput.type = 'checkbox'; + if (defaultValue === true) parameterInput.checked = true; break; case 'array': case 'object': @@ -304,10 +387,13 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput = document.createElement('input'); parameterInput.type = 'text'; parameterInput.className = "form-control"; + if (defaultValue !== undefined) parameterInput.value = defaultValue; } } parameterInput.id = parameter.name; + console.log("defaultValue", defaultValue); + console.log("parameterInput", parameterInput); if (operationSettings[operation] && operationSettings[operation][parameter.name] !== undefined) { let savedValue = operationSettings[operation][parameter.name]; @@ -327,7 +413,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function() parameterInput.value = savedValue; } } - + console.log("parameterInput2", parameterInput); parameterDiv.appendChild(parameterInput); pipelineSettingsContent.appendChild(parameterDiv); @@ -340,50 +426,65 @@ document.getElementById('addOperationBtn').addEventListener('click', function() event.preventDefault(); let settings = {}; operationData.forEach(parameter => { - let value = document.getElementById(parameter.name).value; - switch (parameter.schema.type) { - case 'number': - case 'integer': - settings[parameter.name] = Number(value); - break; - case 'boolean': - settings[parameter.name] = document.getElementById(parameter.name).checked; - break; - case 'array': - case 'object': - try { - settings[parameter.name] = JSON.parse(value); - } catch (err) { - console.error(`Invalid JSON format for ${parameter.name}`); - } - break; - default: - settings[parameter.name] = value; + if(parameter.name !== "fileInput"){ + let value = document.getElementById(parameter.name).value; + switch (parameter.schema.type) { + case 'number': + case 'integer': + settings[parameter.name] = Number(value); + break; + case 'boolean': + settings[parameter.name] = document.getElementById(parameter.name).checked; + break; + case 'array': + case 'object': + try { + settings[parameter.name] = JSON.parse(value); + } catch (err) { + console.error(`Invalid JSON format for ${parameter.name}`); + } + break; + default: + settings[parameter.name] = value; + } } }); operationSettings[operation] = settings; - console.log(settings); - pipelineSettingsModal.style.display = "none"; + //pipelineSettingsModal.style.display = "none"; }); pipelineSettingsContent.appendChild(saveButton); - pipelineSettingsModal.style.display = "block"; + //pipelineSettingsModal.style.display = "block"; - pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { - pipelineSettingsModal.style.display = "none"; - } + //pipelineSettingsModal.getElementsByClassName("close")[0].onclick = function() { + // pipelineSettingsModal.style.display = "none"; + //} - window.onclick = function(event) { - if (event.target == pipelineSettingsModal) { - pipelineSettingsModal.style.display = "none"; - } - } + //window.onclick = function(event) { + // if (event.target == pipelineSettingsModal) { + // 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) { return; } + var pipelineName = document.getElementById('pipelineName').value; let pipelineList = document.getElementById('pipelineList').children; let pipelineConfig = { @@ -393,31 +494,33 @@ document.getElementById('addOperationBtn').addEventListener('click', function() "outputDir": "{outputFolder}/{folderName}", "outputFileName": "{filename}-{pipelineName}-{date}-{time}" }, - "outputDir": "httpWebRequest", + "outputDir": "{outputFolder}", "outputFileName": "{filename}" }; for (let i = 0; i < pipelineList.length; i++) { let operationName = pipelineList[i].querySelector('.operationName').textContent; let parameters = operationSettings[operationName] || {}; - + + parameters['fileInput'] = 'automated'; + pipelineConfig.pipeline.push({ "operation": operationName, "parameters": parameters }); } - + console.log("Downloading.."); let a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([JSON.stringify(pipelineConfig, null, 2)], { type: 'application/json' })); - a.download = 'pipelineConfig.json'; + a.download = pipelineName + '.json'; a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); - }); + } async function processPipelineConfig(configString) { let pipelineConfig = JSON.parse(configString); @@ -483,6 +586,7 @@ document.getElementById('addOperationBtn').addEventListener('click', function() processPipelineConfig(event.target.result); }; reader.readAsText(e.target.files[0]); + hideOrShowPipelineHeader(); }); document.getElementById('pipelineSelect').addEventListener('change', function(e) { @@ -491,4 +595,15 @@ document.getElementById('addOperationBtn').addEventListener('click', function() }); -}); \ No newline at end of file + 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'; + } + } diff --git a/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap b/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap index 7588cec8..9cfbf508 100644 --- a/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap +++ b/src/main/resources/static/pdfjs/cmaps/CNS2-V.bcmap @@ -1,3 +1,3 @@ -RCopyright 1990-2009 Adobe Systems Incorporated. -All rights reserved. +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. See ./LICENSECNS2-H \ No newline at end of file diff --git a/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap b/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap index a7d69db5..c76f5f98 100644 --- a/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap +++ b/src/main/resources/static/pdfjs/cmaps/ETenms-B5-H.bcmap @@ -1,3 +1,3 @@ -RCopyright 1990-2009 Adobe Systems Incorporated. -All rights reserved. +RCopyright 1990-2009 Adobe Systems Incorporated. +All rights reserved. See ./LICENSE ETen-B5-H` ^ \ No newline at end of file diff --git a/src/main/resources/templates/account.html b/src/main/resources/templates/account.html index 83303057..c0e7a757 100644 --- a/src/main/resources/templates/account.html +++ b/src/main/resources/templates/account.html @@ -306,7 +306,7 @@
diff --git a/src/main/resources/templates/convert/pdf-to-csv.html b/src/main/resources/templates/convert/pdf-to-csv.html index d0ff04c8..5cfceeb6 100644 --- a/src/main/resources/templates/convert/pdf-to-csv.html +++ b/src/main/resources/templates/convert/pdf-to-csv.html @@ -13,7 +13,7 @@

-
+
diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 31519548..2ddb3679 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -32,12 +32,12 @@
- +