diff --git a/.github/workflows/PR-Demo-Comment.yml b/.github/workflows/PR-Demo-Comment.yml index bad926a8a..113e690d9 100644 --- a/.github/workflows/PR-Demo-Comment.yml +++ b/.github/workflows/PR-Demo-Comment.yml @@ -115,7 +115,7 @@ jobs: echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbac6d3a9..a513a9c0e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,7 +49,7 @@ jobs: - name: Upload Test Reports if: always() - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: test-reports-jdk-${{ matrix.jdk-version }} path: | @@ -80,7 +80,7 @@ jobs: - name: FAILED - check the licenses for compatibility if: failure() - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dependencies-without-allowed-license.json path: | diff --git a/.github/workflows/licenses-update.yml b/.github/workflows/licenses-update.yml index 64f4527f6..96bdfd411 100644 --- a/.github/workflows/licenses-update.yml +++ b/.github/workflows/licenses-update.yml @@ -24,7 +24,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 + uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -45,7 +45,7 @@ jobs: - name: FAILED - check the licenses for compatibility if: failure() - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: dependencies-without-allowed-license.json path: | diff --git a/.github/workflows/multiOSReleases.yml b/.github/workflows/multiOSReleases.yml index f11f34e55..12c36510f 100644 --- a/.github/workflows/multiOSReleases.yml +++ b/.github/workflows/multiOSReleases.yml @@ -80,7 +80,7 @@ jobs: mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar - name: Upload build artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 1 if-no-files-found: error @@ -106,7 +106,7 @@ jobs: egress-policy: audit - name: Download build artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: stirling-${{ matrix.file_suffix }}binaries @@ -114,7 +114,7 @@ jobs: run: ls -R - name: Upload signed artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 1 if-no-files-found: error @@ -188,7 +188,7 @@ jobs: run: ls -R ./binaries - name: Upload build artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 1 if-no-files-found: error @@ -215,7 +215,7 @@ jobs: egress-policy: audit - name: Download build artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: ${{ matrix.platform }}binaries @@ -255,7 +255,7 @@ jobs: run: ls -R - name: Upload signed artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: retention-days: 1 if-no-files-found: error @@ -276,7 +276,7 @@ jobs: egress-policy: audit - name: Download signed artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 - name: Display structure of downloaded files run: ls -R - name: Upload binaries, attestations and signatures to Release and create GitHub Release diff --git a/.github/workflows/pre_commit.yml b/.github/workflows/pre_commit.yml index b395153ee..f99e9bfc9 100644 --- a/.github/workflows/pre_commit.yml +++ b/.github/workflows/pre_commit.yml @@ -22,7 +22,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 + uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/push-docker.yml b/.github/workflows/push-docker.yml index dffc104c9..dfddd9b15 100644 --- a/.github/workflows/push-docker.yml +++ b/.github/workflows/push-docker.yml @@ -55,13 +55,13 @@ jobs: run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} - name: Login to GitHub Container Registry - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/releaseArtifacts.yml b/.github/workflows/releaseArtifacts.yml index 834890c61..6253b8eb0 100644 --- a/.github/workflows/releaseArtifacts.yml +++ b/.github/workflows/releaseArtifacts.yml @@ -63,7 +63,7 @@ jobs: ls -R ./build/launch4j - name: Upload build artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: binaries${{ matrix.file_suffix }} path: | @@ -88,7 +88,7 @@ jobs: egress-policy: audit - name: Download build artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: binaries${{ matrix.file_suffix }} - name: Display structure of downloaded files @@ -139,7 +139,7 @@ jobs: ./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe - name: Upload signed artifacts - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: signed${{ matrix.file_suffix }} path: | @@ -166,7 +166,7 @@ jobs: egress-policy: audit - name: Download signed artifacts - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: signed${{ matrix.file_suffix }} diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 81ee15aa4..37ff7cea6 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -66,7 +66,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: SARIF file path: results.sarif @@ -74,6 +74,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/upload-sarif@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: sarif_file: results.sarif diff --git a/.github/workflows/sonarqube.yml b/.github/workflows/sonarqube.yml index 8881fcc44..0925d94c7 100644 --- a/.github/workflows/sonarqube.yml +++ b/.github/workflows/sonarqube.yml @@ -46,7 +46,7 @@ jobs: - name: Upload Problems Report on Failure if: failure() - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: gradle-problems-report path: build/reports/problems/problems-report.html @@ -54,7 +54,7 @@ jobs: - name: Upload Sonar Logs on Failure if: failure() - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sonar-logs path: | diff --git a/.github/workflows/sync_files.yml b/.github/workflows/sync_files.yml index 7b43a2175..18158d3dd 100644 --- a/.github/workflows/sync_files.yml +++ b/.github/workflows/sync_files.yml @@ -30,7 +30,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 + uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 with: app-id: ${{ secrets.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} @@ -63,7 +63,7 @@ jobs: - name: Generate GitHub App Token id: generate-token - uses: actions/create-github-app-token@21cfef2b496dd8ef5b904c159339626a10ad380e # v1.11.6 + uses: actions/create-github-app-token@af35edadc00be37caa72ed9f3e6d5f7801bfdf09 # v1.11.7 with: app-id: ${{ vars.GH_APP_ID }} private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} diff --git a/.github/workflows/testdriver.yml b/.github/workflows/testdriver.yml index 2b9685d7e..3c6c216ef 100644 --- a/.github/workflows/testdriver.yml +++ b/.github/workflows/testdriver.yml @@ -40,7 +40,7 @@ jobs: echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT - name: Login to Docker Hub - uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0 + uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_API }} diff --git a/Dockerfile b/Dockerfile index ccb8408a9..46cae3478 100644 --- a/Dockerfile +++ b/Dockerfile @@ -66,6 +66,10 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a poppler-utils \ # OCR MY PDF (unpaper for descew and other advanced features) tesseract-ocr-data-eng \ + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ # CV py3-opencv \ python3 \ diff --git a/Dockerfile.fat b/Dockerfile.fat index 8855be6c0..8a4d55d80 100644 --- a/Dockerfile.fat +++ b/Dockerfile.fat @@ -75,7 +75,10 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a # OCR MY PDF (unpaper for descew and other advanced featues) qpdf \ tesseract-ocr-data-eng \ - + tesseract-ocr-data-chi_sim \ + tesseract-ocr-data-deu \ + tesseract-ocr-data-fra \ + tesseract-ocr-data-por \ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \ # CV py3-opencv \ diff --git a/README.md b/README.md index a152fddb5..423532fe7 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Stirling-PDF currently supports 39 languages! | Dutch (Nederlands) (nl_NL) | ![83%](https://geps.dev/progress/83) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) | -| French (Français) (fr_FR) | ![96%](https://geps.dev/progress/96) | +| French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) | | German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | Greek (Ελληνικά) (el_GR) | ![96%](https://geps.dev/progress/96) | | Hindi (हिंदी) (hi_IN) | ![96%](https://geps.dev/progress/96) | @@ -141,7 +141,7 @@ Stirling-PDF currently supports 39 languages! | Persian (فارسی) (fa_IR) | ![92%](https://geps.dev/progress/92) | | Polish (Polski) (pl_PL) | ![84%](https://geps.dev/progress/84) | | Portuguese (Português) (pt_PT) | ![96%](https://geps.dev/progress/96) | -| Portuguese Brazilian (Português) (pt_BR) | ![97%](https://geps.dev/progress/97) | +| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) | | Romanian (Română) (ro_RO) | ![79%](https://geps.dev/progress/79) | | Russian (Русский) (ru_RU) | ![96%](https://geps.dev/progress/96) | | Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![63%](https://geps.dev/progress/63) | @@ -154,7 +154,7 @@ Stirling-PDF currently supports 39 languages! | Tibetan (བོད་ཡིག་) (zh_BO) | ![93%](https://geps.dev/progress/93) | | Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) | | Turkish (Türkçe) (tr_TR) | ![81%](https://geps.dev/progress/81) | -| Ukrainian (Українська) (uk_UA) | ![97%](https://geps.dev/progress/97) | +| Ukrainian (Українська) (uk_UA) | ![99%](https://geps.dev/progress/99) | | Vietnamese (Tiếng Việt) (vi_VN) | ![78%](https://geps.dev/progress/78) | diff --git a/build.gradle b/build.gradle index f883c55ea..b117c64d2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id "java" - id "org.springframework.boot" version "3.4.3" + id "org.springframework.boot" version "3.4.4" id "io.spring.dependency-management" version "1.1.7" id "org.springdoc.openapi-gradle-plugin" version "1.9.0" id "io.swagger.swaggerhub" version "1.3.2" @@ -15,17 +15,17 @@ plugins { import com.github.jk1.license.render.* ext { - springBootVersion = "3.4.3" + springBootVersion = "3.4.4" pdfboxVersion = "3.0.4" imageioVersion = "3.12.0" lombokVersion = "1.18.36" bouncycastleVersion = "1.80" - springSecuritySamlVersion = "6.4.3" + springSecuritySamlVersion = "6.4.4" openSamlVersion = "4.3.2" } group = "stirling.software" -version = "0.44.2" +version = "0.44.3" java { // 17 is lowest but we support and recommend 21 @@ -299,8 +299,8 @@ configurations.all { dependencies { //tmp for security bumps - implementation 'ch.qos.logback:logback-core:1.5.17' - implementation 'ch.qos.logback:logback-classic:1.5.17' + implementation 'ch.qos.logback:logback-core:1.5.18' + implementation 'ch.qos.logback:logback-classic:1.5.18' // Exclude vulnerable BouncyCastle version used in tableau @@ -317,7 +317,7 @@ dependencies { } //security updates - implementation "org.springframework:spring-webmvc:6.2.3" + implementation "org.springframework:spring-webmvc:6.2.5" implementation("io.github.pixee:java-security-toolkit:1.2.1") @@ -337,7 +337,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.session:spring-session-core:3.4.2" - implementation "org.springframework:spring-jdbc:6.2.3" + implementation "org.springframework:spring-jdbc:6.2.5" implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5' // Don't upgrade h2database diff --git a/src/main/java/stirling/software/SPDF/SPDFApplication.java b/src/main/java/stirling/software/SPDF/SPDFApplication.java index 5ac9f663e..3cf89a657 100644 --- a/src/main/java/stirling/software/SPDF/SPDFApplication.java +++ b/src/main/java/stirling/software/SPDF/SPDFApplication.java @@ -37,6 +37,7 @@ public class SPDFApplication { private static String serverPortStatic; private static String baseUrlStatic; + private static String contextPathStatic; private final Environment env; private final ApplicationProperties applicationProperties; @@ -45,6 +46,9 @@ public class SPDFApplication { @Value("${baseUrl:http://localhost}") private String baseUrl; + @Value("${server.servlet.context-path:/}") + private String contextPath; + public SPDFApplication( Environment env, ApplicationProperties applicationProperties, @@ -138,7 +142,8 @@ public class SPDFApplication { @PostConstruct public void init() { baseUrlStatic = this.baseUrl; - String url = baseUrl + ":" + getStaticPort(); + contextPathStatic = this.contextPath; + String url = baseUrl + ":" + getStaticPort() + contextPath; if (webBrowser != null && Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) { webBrowser.initWebUI(url); @@ -195,7 +200,7 @@ public class SPDFApplication { private static void printStartupLogs() { log.info("Stirling-PDF Started."); - String url = baseUrlStatic + ":" + getStaticPort(); + String url = baseUrlStatic + ":" + getStaticPort() + contextPathStatic; log.info("Navigate to {}", url); } @@ -220,4 +225,8 @@ public class SPDFApplication { public static String getStaticPort() { return serverPortStatic; } + + public static String getStaticContextPath() { + return contextPathStatic; + } } diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 58719f1d0..46c580369 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Properties; import java.util.function.Predicate; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; @@ -78,6 +79,11 @@ public class AppConfig { return applicationProperties.getUi().getLanguages(); } + @Bean + public String contextPath(@Value("${server.servlet.context-path}") String contextPath) { + return contextPath; + } + @Bean(name = "navBarText") public String navBarText() { String defaultNavBar = diff --git a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java index 12bf2c291..5b4eb2382 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java @@ -48,6 +48,22 @@ public class EndpointConfiguration { return endpointStatuses.getOrDefault(endpoint, true); } + public boolean isGroupEnabled(String group) { + Set endpoints = endpointGroups.get(group); + if (endpoints == null || endpoints.isEmpty()) { + log.debug("Group '{}' does not exist or has no endpoints", group); + return false; + } + + for (String endpoint : endpoints) { + if (!isEndpointEnabled(endpoint)) { + return false; + } + } + + return true; + } + public void addEndpointToGroup(String group, String endpoint) { endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); } @@ -176,21 +192,17 @@ public class EndpointConfiguration { addEndpointToGroup("OpenCV", "extract-image-scans"); // LibreOffice - addEndpointToGroup("qpdf", "repair"); addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("LibreOffice", "pdf-to-rtf"); addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-xml"); + addEndpointToGroup("LibreOffice", "pdf-to-pdfa"); // Unoconvert addEndpointToGroup("Unoconvert", "file-to-pdf"); - // qpdf - addEndpointToGroup("qpdf", "compress-pdf"); - addEndpointToGroup("qpdf", "pdf-to-pdfa"); - addEndpointToGroup("tesseract", "ocr-pdf"); // Java @@ -240,8 +252,6 @@ public class EndpointConfiguration { addEndpointToGroup("Javascript", "adjust-contrast"); // qpdf dependent endpoints - addEndpointToGroup("qpdf", "compress-pdf"); - addEndpointToGroup("qpdf", "pdf-to-pdfa"); addEndpointToGroup("qpdf", "repair"); // Weasyprint dependent endpoints diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInspector.java b/src/main/java/stirling/software/SPDF/config/EndpointInspector.java new file mode 100644 index 000000000..474606783 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/EndpointInspector.java @@ -0,0 +1,216 @@ +package stirling.software.SPDF.config; + +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +@Component +public class EndpointInspector implements ApplicationListener { + private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class); + + private final ApplicationContext applicationContext; + private final Set validGetEndpoints = new HashSet<>(); + private boolean endpointsDiscovered = false; + + @Autowired + public EndpointInspector(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (!endpointsDiscovered) { + discoverEndpoints(); + endpointsDiscovered = true; + logger.info("Discovered {} valid GET endpoints", validGetEndpoints.size()); + } + } + + private void discoverEndpoints() { + try { + Map mappings = + applicationContext.getBeansOfType(RequestMappingHandlerMapping.class); + + for (Map.Entry entry : mappings.entrySet()) { + RequestMappingHandlerMapping mapping = entry.getValue(); + Map handlerMethods = mapping.getHandlerMethods(); + + for (Map.Entry handlerEntry : + handlerMethods.entrySet()) { + RequestMappingInfo mappingInfo = handlerEntry.getKey(); + HandlerMethod handlerMethod = handlerEntry.getValue(); + + boolean isGetHandler = false; + try { + Set methods = mappingInfo.getMethodsCondition().getMethods(); + isGetHandler = methods.isEmpty() || methods.contains(RequestMethod.GET); + } catch (Exception e) { + isGetHandler = true; + } + + if (isGetHandler) { + Set patterns = extractPatternsUsingDirectPaths(mappingInfo); + + if (patterns.isEmpty()) { + patterns = extractPatternsFromString(mappingInfo); + } + + validGetEndpoints.addAll(patterns); + } + } + } + + if (validGetEndpoints.isEmpty()) { + logger.warn("No endpoints discovered. Adding common endpoints as fallback."); + validGetEndpoints.add("/"); + validGetEndpoints.add("/api/**"); + validGetEndpoints.add("/**"); + } + } catch (Exception e) { + logger.error("Error discovering endpoints", e); + } + } + + private Set extractPatternsUsingDirectPaths(RequestMappingInfo mappingInfo) { + Set patterns = new HashSet<>(); + + try { + Method getDirectPathsMethod = mappingInfo.getClass().getMethod("getDirectPaths"); + Object result = getDirectPathsMethod.invoke(mappingInfo); + if (result instanceof Set) { + @SuppressWarnings("unchecked") + Set resultSet = (Set) result; + patterns.addAll(resultSet); + } + } catch (Exception e) { + // Return empty set if method not found or fails + } + + return patterns; + } + + private Set extractPatternsFromString(RequestMappingInfo mappingInfo) { + Set patterns = new HashSet<>(); + try { + String infoString = mappingInfo.toString(); + if (infoString.contains("{")) { + String patternsSection = + infoString.substring(infoString.indexOf("{") + 1, infoString.indexOf("}")); + + for (String pattern : patternsSection.split(",")) { + pattern = pattern.trim(); + if (!pattern.isEmpty()) { + patterns.add(pattern); + } + } + } + } catch (Exception e) { + // Return empty set if parsing fails + } + return patterns; + } + + public boolean isValidGetEndpoint(String uri) { + if (!endpointsDiscovered) { + discoverEndpoints(); + endpointsDiscovered = true; + } + + if (validGetEndpoints.contains(uri)) { + return true; + } + + if (matchesWildcardOrPathVariable(uri)) { + return true; + } + + if (matchesPathSegments(uri)) { + return true; + } + + return false; + } + + private boolean matchesWildcardOrPathVariable(String uri) { + for (String pattern : validGetEndpoints) { + if (pattern.contains("*") || pattern.contains("{")) { + int wildcardIndex = pattern.indexOf('*'); + int variableIndex = pattern.indexOf('{'); + + int cutoffIndex; + if (wildcardIndex < 0) { + cutoffIndex = variableIndex; + } else if (variableIndex < 0) { + cutoffIndex = wildcardIndex; + } else { + cutoffIndex = Math.min(wildcardIndex, variableIndex); + } + + String staticPrefix = pattern.substring(0, cutoffIndex); + + if (uri.startsWith(staticPrefix)) { + return true; + } + } + } + return false; + } + + private boolean matchesPathSegments(String uri) { + for (String pattern : validGetEndpoints) { + if (!pattern.contains("*") && !pattern.contains("{")) { + String[] patternSegments = pattern.split("/"); + String[] uriSegments = uri.split("/"); + + if (uriSegments.length < patternSegments.length) { + continue; + } + + boolean match = true; + for (int i = 0; i < patternSegments.length; i++) { + if (!patternSegments[i].equals(uriSegments[i])) { + match = false; + break; + } + } + + if (match) { + return true; + } + } + } + return false; + } + + public Set getValidGetEndpoints() { + if (!endpointsDiscovered) { + discoverEndpoints(); + endpointsDiscovered = true; + } + return new HashSet<>(validGetEndpoints); + } + + private void logAllEndpoints() { + Set sortedEndpoints = new TreeSet<>(validGetEndpoints); + + logger.info("=== BEGIN: All discovered GET endpoints ==="); + for (String endpoint : sortedEndpoints) { + logger.info("Endpoint: {}", endpoint); + } + logger.info("=== END: All discovered GET endpoints ==="); + } +} 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 3a9fd3c23..c676169c8 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/UserController.java +++ b/src/main/java/stirling/software/SPDF/controller/api/UserController.java @@ -333,7 +333,7 @@ public class UserController { } // Invalidate all sessions before deleting the user List sessionsInformations = - sessionRegistry.getAllSessions(authentication.getPrincipal(), false); + sessionRegistry.getAllSessions(username, false); for (SessionInformation sessionsInformation : sessionsInformations) { sessionRegistry.expireSession(sessionsInformation.getSessionId()); sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId()); 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 7d1985cec..79d46cc45 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 @@ -7,6 +7,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -29,8 +30,8 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PostMapping; @@ -44,13 +45,14 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; +import stirling.software.SPDF.config.EndpointConfiguration; import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; import stirling.software.SPDF.service.CustomPDFDocumentFactory; import stirling.software.SPDF.utils.GeneralUtils; -import stirling.software.SPDF.utils.ImageProcessingUtils; import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.WebResponseUtils; @@ -62,10 +64,13 @@ import stirling.software.SPDF.utils.WebResponseUtils; public class CompressController { private final CustomPDFDocumentFactory pdfDocumentFactory; + private final boolean qpdfEnabled; - @Autowired - public CompressController(CustomPDFDocumentFactory pdfDocumentFactory) { + public CompressController( + CustomPDFDocumentFactory pdfDocumentFactory, + EndpointConfiguration endpointConfiguration) { this.pdfDocumentFactory = pdfDocumentFactory; + this.qpdfEnabled = endpointConfiguration.isGroupEnabled("qpdf"); } @Data @@ -76,10 +81,30 @@ public class CompressController { COSName name; // The name used to reference this image } + @Data + @EqualsAndHashCode(callSuper = true) + @AllArgsConstructor + @NoArgsConstructor + private static class NestedImageReference extends ImageReference { + COSName formName; // Name of the form XObject containing the image + COSName imageName; // Name of the image within the form + } + + // Tracks compression stats for reporting + private static class CompressionStats { + int totalImages = 0; + int nestedImages = 0; + int uniqueImagesCount = 0; + int compressedImages = 0; + int skippedImages = 0; + long totalOriginalBytes = 0; + long totalCompressedBytes = 0; + } + public Path compressImagesInPDF( Path pdfFile, double scaleFactor, float jpegQuality, boolean convertToGrayscale) throws Exception { - Path newCompressedPDF = Files.createTempFile("compressedPDF", ".pdf"); + Path newCompressedPDF = Files.createTempFile("compressedPDF", ".pdf"); long originalFileSize = Files.size(pdfFile); log.info( "Starting image compression with scale factor: {}, JPEG quality: {}, grayscale: {} on file size: {}", @@ -89,146 +114,29 @@ public class CompressController { GeneralUtils.formatBytes(originalFileSize)); try (PDDocument doc = pdfDocumentFactory.load(pdfFile)) { + // Find all unique images in the document + Map> uniqueImages = findImages(doc); - // Collect all unique images by content hash - Map> uniqueImages = new HashMap<>(); - Map compressedVersions = new HashMap<>(); + // Get statistics + CompressionStats stats = new CompressionStats(); + stats.uniqueImagesCount = uniqueImages.size(); + calculateImageStats(uniqueImages, stats); - int totalImages = 0; + // Create compressed versions of unique images + Map compressedVersions = + createCompressedImages( + doc, uniqueImages, scaleFactor, jpegQuality, convertToGrayscale, stats); - for (int pageNum = 0; pageNum < doc.getNumberOfPages(); pageNum++) { - PDPage page = doc.getPage(pageNum); - PDResources res = page.getResources(); - if (res == null || res.getXObjectNames() == null) continue; - - for (COSName name : res.getXObjectNames()) { - PDXObject xobj = res.getXObject(name); - if (!(xobj instanceof PDImageXObject)) continue; - - totalImages++; - PDImageXObject image = (PDImageXObject) xobj; - String imageHash = generateImageHash(image); - - // Store only page number and name reference - ImageReference ref = new ImageReference(); - ref.pageNum = pageNum; - ref.name = name; - - uniqueImages.computeIfAbsent(imageHash, k -> new ArrayList<>()).add(ref); - } - } - - int uniqueImagesCount = uniqueImages.size(); - int duplicatedImages = totalImages - uniqueImagesCount; - log.info( - "Found {} unique images and {} duplicated instances across {} pages", - uniqueImagesCount, - duplicatedImages, - doc.getNumberOfPages()); - - // SECOND PASS: Process each unique image exactly once - int compressedImages = 0; - int skippedImages = 0; - long totalOriginalBytes = 0; - long totalCompressedBytes = 0; - - for (Entry> entry : uniqueImages.entrySet()) { - String imageHash = entry.getKey(); - List references = entry.getValue(); - - if (references.isEmpty()) continue; - - // Get the first instance of this image - ImageReference firstRef = references.get(0); - PDPage firstPage = doc.getPage(firstRef.pageNum); - PDResources firstPageResources = firstPage.getResources(); - PDImageXObject originalImage = - (PDImageXObject) firstPageResources.getXObject(firstRef.name); - - // Track original size - int originalSize = (int) originalImage.getCOSObject().getLength(); - totalOriginalBytes += originalSize; - - // Process this unique image once - BufferedImage processedImage = - processAndCompressImage( - originalImage, scaleFactor, jpegQuality, convertToGrayscale); - - if (processedImage != null) { - // Convert to bytes for storage - byte[] compressedData = convertToBytes(processedImage, jpegQuality); - - // Check if compression is beneficial - if (compressedData.length < originalSize || convertToGrayscale) { - // Create a single compressed version - PDImageXObject compressedImage = - PDImageXObject.createFromByteArray( - doc, - compressedData, - originalImage.getCOSObject().toString()); - - // Store the compressed version only once in our map - compressedVersions.put(imageHash, compressedImage); - - // Report compression stats - double reductionPercentage = - 100.0 - ((compressedData.length * 100.0) / originalSize); - log.info( - "Image hash {}: Compressed from {} to {} (reduced by {}%)", - imageHash, - GeneralUtils.formatBytes(originalSize), - GeneralUtils.formatBytes(compressedData.length), - String.format("%.1f", reductionPercentage)); - - // Replace ALL instances with the compressed version - for (ImageReference ref : references) { - // Get the page and resources when needed - PDPage page = doc.getPage(ref.pageNum); - PDResources resources = page.getResources(); - resources.put(ref.name, compressedImage); - - log.info( - "Replaced image on page {} with compressed version", - ref.pageNum + 1); - } - - totalCompressedBytes += compressedData.length * references.size(); - compressedImages++; - } else { - log.info("Image hash {}: Compression not beneficial, skipping", imageHash); - totalCompressedBytes += originalSize * references.size(); - skippedImages++; - } - } else { - log.info("Image hash {}: Not suitable for compression, skipping", imageHash); - totalCompressedBytes += originalSize * references.size(); - skippedImages++; - } - } + // Replace all instances with compressed versions + replaceImages(doc, uniqueImages, compressedVersions, stats); // Log compression statistics - double overallImageReduction = - totalOriginalBytes > 0 - ? 100.0 - ((totalCompressedBytes * 100.0) / totalOriginalBytes) - : 0; - - log.info( - "Image compression summary - Total unique: {}, Compressed: {}, Skipped: {}, Duplicates: {}", - uniqueImagesCount, - compressedImages, - skippedImages, - duplicatedImages); - log.info( - "Total original image size: {}, compressed: {} (reduced by {}%)", - GeneralUtils.formatBytes(totalOriginalBytes), - GeneralUtils.formatBytes(totalCompressedBytes), - String.format("%.1f", overallImageReduction)); + logCompressionStats(stats, originalFileSize); // Free memory before saving compressedVersions.clear(); uniqueImages.clear(); - // Save the document log.info("Saving compressed PDF to {}", newCompressedPDF.toString()); doc.save(newCompressedPDF.toString()); @@ -242,7 +150,315 @@ public class CompressController { String.format("%.1f", overallReduction)); return newCompressedPDF; } - + } + + // Find all images in the document, both direct and nested within forms + private Map> findImages(PDDocument doc) throws IOException { + Map> uniqueImages = new HashMap<>(); + + // Scan through all pages in the document + for (int pageNum = 0; pageNum < doc.getNumberOfPages(); pageNum++) { + PDPage page = doc.getPage(pageNum); + PDResources res = page.getResources(); + if (res == null || res.getXObjectNames() == null) continue; + + // Process all XObjects on the page + for (COSName name : res.getXObjectNames()) { + PDXObject xobj = res.getXObject(name); + + // Direct image + if (isImage(xobj)) { + addDirectImage(pageNum, name, (PDImageXObject) xobj, uniqueImages); + log.info( + "Found direct image '{}' on page {} - {}x{}", + name.getName(), + pageNum + 1, + ((PDImageXObject) xobj).getWidth(), + ((PDImageXObject) xobj).getHeight()); + } + // Form XObject that may contain nested images + else if (isForm(xobj)) { + checkFormForImages(pageNum, name, (PDFormXObject) xobj, uniqueImages); + } + } + } + + return uniqueImages; + } + + private boolean isImage(PDXObject xobj) { + return xobj instanceof PDImageXObject; + } + + private boolean isForm(PDXObject xobj) { + return xobj instanceof PDFormXObject; + } + + private ImageReference addDirectImage( + int pageNum, + COSName name, + PDImageXObject image, + Map> uniqueImages) + throws IOException { + ImageReference ref = new ImageReference(); + ref.pageNum = pageNum; + ref.name = name; + + String imageHash = generateImageHash(image); + uniqueImages.computeIfAbsent(imageHash, k -> new ArrayList<>()).add(ref); + + return ref; + } + + // Look for images inside form XObjects + private void checkFormForImages( + int pageNum, + COSName formName, + PDFormXObject formXObj, + Map> uniqueImages) + throws IOException { + PDResources formResources = formXObj.getResources(); + if (formResources == null || formResources.getXObjectNames() == null) { + return; + } + + log.info( + "Checking form XObject '{}' on page {} for nested images", + formName.getName(), + pageNum + 1); + + // Process all XObjects within the form + for (COSName nestedName : formResources.getXObjectNames()) { + PDXObject nestedXobj = formResources.getXObject(nestedName); + + if (isImage(nestedXobj)) { + PDImageXObject nestedImage = (PDImageXObject) nestedXobj; + + log.info( + "Found nested image '{}' in form '{}' on page {} - {}x{}", + nestedName.getName(), + formName.getName(), + pageNum + 1, + nestedImage.getWidth(), + nestedImage.getHeight()); + + // Create specialized reference for the nested image + NestedImageReference nestedRef = new NestedImageReference(); + nestedRef.pageNum = pageNum; + nestedRef.formName = formName; + nestedRef.imageName = nestedName; + + String imageHash = generateImageHash(nestedImage); + uniqueImages.computeIfAbsent(imageHash, k -> new ArrayList<>()).add(nestedRef); + } + } + } + + // Count total images and nested images + private void calculateImageStats( + Map> uniqueImages, CompressionStats stats) { + for (List references : uniqueImages.values()) { + for (ImageReference ref : references) { + stats.totalImages++; + if (ref instanceof NestedImageReference) { + stats.nestedImages++; + } + } + } + } + + // Create compressed versions of all unique images + private Map createCompressedImages( + PDDocument doc, + Map> uniqueImages, + double scaleFactor, + float jpegQuality, + boolean convertToGrayscale, + CompressionStats stats) + throws IOException { + + Map compressedVersions = new HashMap<>(); + + // Process each unique image exactly once + for (Entry> entry : uniqueImages.entrySet()) { + String imageHash = entry.getKey(); + List references = entry.getValue(); + + if (references.isEmpty()) continue; + + // Get the first instance of this image + PDImageXObject originalImage = getOriginalImage(doc, references.get(0)); + + // Track original size + int originalSize = (int) originalImage.getCOSObject().getLength(); + stats.totalOriginalBytes += originalSize; + + // Process this unique image + PDImageXObject compressedImage = + compressImage( + doc, + originalImage, + originalSize, + scaleFactor, + jpegQuality, + convertToGrayscale); + + if (compressedImage != null) { + // Store the compressed version in our map + compressedVersions.put(imageHash, compressedImage); + stats.compressedImages++; + + // Update compression stats + int compressedSize = (int) compressedImage.getCOSObject().getLength(); + stats.totalCompressedBytes += compressedSize * references.size(); + + double reductionPercentage = 100.0 - ((compressedSize * 100.0) / originalSize); + log.info( + "Image hash {}: Compressed from {} to {} (reduced by {}%)", + imageHash, + GeneralUtils.formatBytes(originalSize), + GeneralUtils.formatBytes(compressedSize), + String.format("%.1f", reductionPercentage)); + } else { + log.info("Image hash {}: Not suitable for compression, skipping", imageHash); + stats.totalCompressedBytes += originalSize * references.size(); + stats.skippedImages++; + } + } + + return compressedVersions; + } + + // Get original image from a reference + private PDImageXObject getOriginalImage(PDDocument doc, ImageReference ref) throws IOException { + if (ref instanceof NestedImageReference) { + // Get the nested image from within a form XObject + NestedImageReference nestedRef = (NestedImageReference) ref; + PDPage page = doc.getPage(nestedRef.pageNum); + PDResources pageResources = page.getResources(); + + // Get the form XObject + PDFormXObject formXObj = (PDFormXObject) pageResources.getXObject(nestedRef.formName); + + // Get the nested image from the form's resources + PDResources formResources = formXObj.getResources(); + return (PDImageXObject) formResources.getXObject(nestedRef.imageName); + } else { + // Get direct image from page resources + PDPage page = doc.getPage(ref.pageNum); + PDResources resources = page.getResources(); + return (PDImageXObject) resources.getXObject(ref.name); + } + } + + // Try to compress an image if it makes sense + private PDImageXObject compressImage( + PDDocument doc, + PDImageXObject originalImage, + int originalSize, + double scaleFactor, + float jpegQuality, + boolean convertToGrayscale) + throws IOException { + + // Process and compress the image + BufferedImage processedImage = + processAndCompressImage( + originalImage, scaleFactor, jpegQuality, convertToGrayscale); + + if (processedImage == null) { + return null; + } + + // Convert to bytes for storage + byte[] compressedData = convertToBytes(processedImage, jpegQuality); + + // Check if compression is beneficial + if (compressedData.length < originalSize || convertToGrayscale) { + // Create a compressed version + return PDImageXObject.createFromByteArray( + doc, compressedData, originalImage.getCOSObject().toString()); + } + + return null; + } + + // Replace all instances of original images with their compressed versions + private void replaceImages( + PDDocument doc, + Map> uniqueImages, + Map compressedVersions, + CompressionStats stats) + throws IOException { + + for (Entry> entry : uniqueImages.entrySet()) { + String imageHash = entry.getKey(); + List references = entry.getValue(); + + // Skip if no compressed version exists + PDImageXObject compressedImage = compressedVersions.get(imageHash); + if (compressedImage == null) continue; + + // Replace ALL instances with the compressed version + for (ImageReference ref : references) { + replaceImageReference(doc, ref, compressedImage); + } + } + } + + // Replace a specific image reference with a compressed version + private void replaceImageReference( + PDDocument doc, ImageReference ref, PDImageXObject compressedImage) throws IOException { + if (ref instanceof NestedImageReference) { + // Replace nested image within form XObject + NestedImageReference nestedRef = (NestedImageReference) ref; + PDPage page = doc.getPage(nestedRef.pageNum); + PDResources pageResources = page.getResources(); + + // Get the form XObject + PDFormXObject formXObj = (PDFormXObject) pageResources.getXObject(nestedRef.formName); + + // Replace the nested image in the form's resources + PDResources formResources = formXObj.getResources(); + formResources.put(nestedRef.imageName, compressedImage); + + log.info( + "Replaced nested image '{}' in form '{}' on page {} with compressed version", + nestedRef.imageName.getName(), + nestedRef.formName.getName(), + nestedRef.pageNum + 1); + } else { + // Replace direct image in page resources + PDPage page = doc.getPage(ref.pageNum); + PDResources resources = page.getResources(); + resources.put(ref.name, compressedImage); + + log.info("Replaced direct image on page {} with compressed version", ref.pageNum + 1); + } + } + + // Log final stats about the compression + private void logCompressionStats(CompressionStats stats, long originalFileSize) { + // Calculate image reduction percentage + double overallImageReduction = + stats.totalOriginalBytes > 0 + ? 100.0 - ((stats.totalCompressedBytes * 100.0) / stats.totalOriginalBytes) + : 0; + + int duplicatedImages = stats.totalImages - stats.uniqueImagesCount; + + log.info( + "Image compression summary - Total unique: {}, Compressed: {}, Skipped: {}, Duplicates: {}, Nested: {}", + stats.uniqueImagesCount, + stats.compressedImages, + stats.skippedImages, + duplicatedImages, + stats.nestedImages); + log.info( + "Total original image size: {}, compressed: {} (reduced by {}%)", + GeneralUtils.formatBytes(stats.totalOriginalBytes), + GeneralUtils.formatBytes(stats.totalCompressedBytes), + String.format("%.1f", overallImageReduction)); } private BufferedImage convertToGrayscale(BufferedImage image) { @@ -257,10 +473,7 @@ public class CompressController { return grayImage; } - /** - * Processes and compresses an image if beneficial. Returns the processed image if compression - * is worthwhile, null otherwise. - */ + // Resize and optionally convert to grayscale private BufferedImage processAndCompressImage( PDImageXObject image, double scaleFactor, float jpegQuality, boolean convertToGrayscale) throws IOException { @@ -342,10 +555,7 @@ public class CompressController { return scaledImage; } - /** - * Converts a BufferedImage to a byte array with specified JPEG quality. Checks if compression - * is beneficial compared to original. - */ + // Convert image to byte array with quality settings private byte[] convertToBytes(BufferedImage scaledImage, float jpegQuality) throws IOException { String format = scaledImage.getColorModel().hasAlpha() ? "png" : "jpeg"; ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -376,7 +586,7 @@ public class CompressController { return outputStream.toByteArray(); } - /** Modified hash function to consistently identify identical image content */ + // Hash function to identify identical images private String generateImageHash(PDImageXObject image) { try { // Create a stream for the raw stream data @@ -414,43 +624,26 @@ public class CompressController { } } - private byte[] generateImageMD5(PDImageXObject image) throws IOException { - return generatMD5(ImageProcessingUtils.getImageData(image.getImage())); - } - - /** Generates a hash string from a byte array */ - private String generateHashFromBytes(byte[] data) { - try { - // Use the existing method to generate MD5 hash - byte[] hash = generatMD5(data); - return bytesToHexString(hash); - } catch (Exception e) { - log.error("Error generating hash from bytes", e); - // Return a unique string as fallback - return "fallback-" + System.identityHashCode(data); - } - } - - // Updated scale factor method for levels 4-9 + // Scale factors for different optimization levels private double getScaleFactorForLevel(int optimizeLevel) { return switch (optimizeLevel) { - case 4 -> 0.9; // 90% of original size - lite image compression - case 5 -> 0.8; // 80% of original size - lite image compression - case 6 -> 0.7; // 70% of original size - lite image compression - case 7 -> 0.6; // 60% of original size - intense image compression - case 8 -> 0.5; // 50% of original size - intense image compression - case 9, 10 -> 0.4; // 40% of original size - intense image compression - default -> 1.0; // No image scaling for levels 1-3 + case 4 -> 0.9; // 90% - lite compression + case 5 -> 0.8; // 80% - lite compression + case 6 -> 0.7; // 70% - lite compression + case 7 -> 0.6; // 60% - intense compression + case 8 -> 0.5; // 50% - intense compression + case 9, 10 -> 0.4; // 40% - intense compression + default -> 1.0; // No scaling for levels 1-3 }; } - // New method for JPEG quality based on optimization level + // JPEG quality for different optimization levels private float getJpegQualityForLevel(int optimizeLevel) { return switch (optimizeLevel) { - case 7 -> 0.8f; // 80% quality - intense compression - case 8 -> 0.6f; // 60% quality - more intense compression - case 9, 10 -> 0.4f; // 40% quality - most intense compression - default -> 0.7f; // 70% quality for levels 1-6 (higher quality) + case 7 -> 0.8f; // 80% quality + case 8 -> 0.6f; // 60% quality + case 9, 10 -> 0.4f; // 40% quality + default -> 0.7f; // 70% quality for levels 1-6 }; } @@ -478,17 +671,17 @@ public class CompressController { } // Create initial input file - Path originalFile = Files.createTempFile("input_", ".pdf"); + Path originalFile = Files.createTempFile("original_", ".pdf"); inputFile.transferTo(originalFile.toFile()); long inputFileSize = Files.size(originalFile); - - // Start with original as current working file - Path currentFile = originalFile; - + + Path currentFile = Files.createTempFile("working_", ".pdf"); + Files.copy(originalFile, currentFile, StandardCopyOption.REPLACE_EXISTING); + // Keep track of all temporary files for cleanup List tempFiles = new ArrayList<>(); tempFiles.add(originalFile); - + tempFiles.add(currentFile); try { if (autoMode) { double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; @@ -499,93 +692,56 @@ public class CompressController { boolean imageCompressionApplied = false; boolean qpdfCompressionApplied = false; + if (qpdfEnabled && optimizeLevel <= 3) { + optimizeLevel = 4; + } + while (!sizeMet && optimizeLevel <= 9) { // Apply image compression for levels 4-9 if ((optimizeLevel >= 4 || Boolean.TRUE.equals(convertToGrayscale)) && !imageCompressionApplied) { double scaleFactor = getScaleFactorForLevel(optimizeLevel); float jpegQuality = getJpegQualityForLevel(optimizeLevel); - - // Use the returned path from compressImagesInPDF - Path compressedImageFile = compressImagesInPDF( - currentFile, - scaleFactor, - jpegQuality, - Boolean.TRUE.equals(convertToGrayscale)); - - // Add to temp files list and update current file + + // Compress images + Path compressedImageFile = + compressImagesInPDF( + currentFile, + scaleFactor, + jpegQuality, + Boolean.TRUE.equals(convertToGrayscale)); + tempFiles.add(compressedImageFile); currentFile = compressedImageFile; imageCompressionApplied = true; } // Apply QPDF compression for all levels - if (!qpdfCompressionApplied) { - long preQpdfSize = Files.size(currentFile); - log.info("Pre-QPDF file size: {}", GeneralUtils.formatBytes(preQpdfSize)); - - // Map optimization levels to QPDF compression levels - int qpdfCompressionLevel = optimizeLevel <= 3 - ? optimizeLevel * 3 // Level 1->3, 2->6, 3->9 - : 9; // Max compression for levels 4-9 - - // Create output file for QPDF - Path qpdfOutputFile = Files.createTempFile("qpdf_output_", ".pdf"); - tempFiles.add(qpdfOutputFile); - - // Run QPDF optimization - List command = new ArrayList<>(); - command.add("qpdf"); - if (request.getNormalize()) { - command.add("--normalize-content=y"); - } - if (request.getLinearize()) { - command.add("--linearize"); - } - command.add("--recompress-flate"); - command.add("--compression-level=" + qpdfCompressionLevel); - command.add("--compress-streams=y"); - command.add("--object-streams=generate"); - command.add(currentFile.toString()); - command.add(qpdfOutputFile.toString()); - - ProcessExecutorResult returnCode = null; - try { - returnCode = ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) - .runCommandWithOutputHandling(command); - qpdfCompressionApplied = true; - - // Update current file to the QPDF output - currentFile = qpdfOutputFile; - - long postQpdfSize = Files.size(currentFile); - double qpdfReduction = 100.0 - ((postQpdfSize * 100.0) / preQpdfSize); - log.info( - "Post-QPDF file size: {} (reduced by {}%)", - GeneralUtils.formatBytes(postQpdfSize), - String.format("%.1f", qpdfReduction)); - - } catch (Exception e) { - if (returnCode != null && returnCode.getRc() != 3) { - throw e; - } - // If QPDF fails, keep using the current file - log.warn("QPDF compression failed, continuing with current file"); + if (!qpdfCompressionApplied && qpdfEnabled) { + applyQpdfCompression(request, optimizeLevel, currentFile, tempFiles); + qpdfCompressionApplied = true; + } else if (!qpdfCompressionApplied) { + // If QPDF is disabled, mark as applied and log + if (!qpdfEnabled) { + log.info("Skipping QPDF compression as QPDF group is disabled"); } + qpdfCompressionApplied = true; } - // Check if file size is within expected size or not auto mode + // Check if target size reached or not in auto mode long outputFileSize = Files.size(currentFile); if (outputFileSize <= expectedOutputSize || !autoMode) { sizeMet = true; } else { - int newOptimizeLevel = incrementOptimizeLevel( - optimizeLevel, outputFileSize, expectedOutputSize); + int newOptimizeLevel = + incrementOptimizeLevel( + optimizeLevel, outputFileSize, expectedOutputSize); // Check if we can't increase the level further if (newOptimizeLevel == optimizeLevel) { if (autoMode) { - log.info("Maximum optimization level reached without meeting target size."); + log.info( + "Maximum optimization level reached without meeting target size."); sizeMet = true; } } else { @@ -597,18 +753,19 @@ public class CompressController { } } - // Check if optimized file is larger than the original + // Use original if optimized file is somehow larger long finalFileSize = Files.size(currentFile); - if (finalFileSize > inputFileSize) { - log.warn("Optimized file is larger than the original. Using the original file instead."); - // Use the stored reference to the original file + if (finalFileSize >= inputFileSize) { + log.warn( + "Optimized file is larger than the original. Using the original file instead."); currentFile = originalFile; } - String outputFilename = Filenames.toSimpleFileName(inputFile.getOriginalFilename()) + String outputFilename = + Filenames.toSimpleFileName(inputFile.getOriginalFilename()) .replaceFirst("[.][^.]+$", "") + "_Optimized.pdf"; - + return WebResponseUtils.pdfDocToWebResponse( pdfDocumentFactory.load(currentFile.toFile()), outputFilename); @@ -624,6 +781,65 @@ public class CompressController { } } + // Run QPDF compression + private void applyQpdfCompression( + OptimizePdfRequest request, int optimizeLevel, Path currentFile, List tempFiles) + throws IOException { + + long preQpdfSize = Files.size(currentFile); + log.info("Pre-QPDF file size: {}", GeneralUtils.formatBytes(preQpdfSize)); + + // Map optimization levels to QPDF compression levels + int qpdfCompressionLevel = + optimizeLevel <= 3 + ? optimizeLevel * 3 // Level 1->3, 2->6, 3->9 + : 9; // Max compression for levels 4-9 + + // Create output file for QPDF + Path qpdfOutputFile = Files.createTempFile("qpdf_output_", ".pdf"); + tempFiles.add(qpdfOutputFile); + + // Build QPDF command + List command = new ArrayList<>(); + command.add("qpdf"); + if (request.getNormalize()) { + command.add("--normalize-content=y"); + } + if (request.getLinearize()) { + command.add("--linearize"); + } + command.add("--recompress-flate"); + command.add("--compression-level=" + qpdfCompressionLevel); + command.add("--compress-streams=y"); + command.add("--object-streams=generate"); + command.add(currentFile.toString()); + command.add(qpdfOutputFile.toString()); + + ProcessExecutorResult returnCode = null; + try { + returnCode = + ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF) + .runCommandWithOutputHandling(command); + + // Update current file to the QPDF output + Files.copy(qpdfOutputFile, currentFile, StandardCopyOption.REPLACE_EXISTING); + + long postQpdfSize = Files.size(currentFile); + double qpdfReduction = 100.0 - ((postQpdfSize * 100.0) / preQpdfSize); + log.info( + "Post-QPDF file size: {} (reduced by {}%)", + GeneralUtils.formatBytes(postQpdfSize), String.format("%.1f", qpdfReduction)); + + } catch (Exception e) { + if (returnCode != null && returnCode.getRc() != 3) { + throw new IOException("QPDF command failed", e); + } + // If QPDF fails, keep using the current file + log.warn("QPDF compression failed, continuing with current file", e); + } + } + + // Pick optimization level based on target size private int determineOptimizeLevel(double sizeReductionRatio) { if (sizeReductionRatio > 0.9) return 1; if (sizeReductionRatio > 0.8) return 2; @@ -636,6 +852,7 @@ public class CompressController { return 9; } + // Increment optimization level if we need more compression private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) { double currentRatio = currentSize / (double) targetSize; log.info("Current compression ratio: {}", String.format("%.2f", currentRatio)); 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 291cd9b45..2ab90cac2 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 @@ -5,6 +5,7 @@ import java.io.InputStream; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -26,8 +27,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import stirling.software.SPDF.model.PipelineConfig; +import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; import stirling.software.SPDF.model.api.HandleDataRequest; +import stirling.software.SPDF.service.PostHogService; import stirling.software.SPDF.utils.WebResponseUtils; @RestController @@ -40,9 +43,13 @@ public class PipelineController { private final ObjectMapper objectMapper; - public PipelineController(PipelineProcessor processor, ObjectMapper objectMapper) { + private final PostHogService postHogService; + + public PipelineController( + PipelineProcessor processor, ObjectMapper objectMapper, PostHogService postHogService) { this.processor = processor; this.objectMapper = objectMapper; + this.postHogService = postHogService; } @PostMapping("/handleData") @@ -55,6 +62,18 @@ public class PipelineController { } PipelineConfig config = objectMapper.readValue(jsonString, PipelineConfig.class); log.info("Received POST request to /handleData with {} files", files.length); + + List operationNames = + config.getOperations().stream() + .map(PipelineOperation::getOperation) + .collect(Collectors.toList()); + + Map properties = new HashMap<>(); + properties.put("operations", operationNames); + properties.put("fileCount", files.length); + + postHogService.captureEvent("pipeline_api_event", properties); + try { List inputFiles = processor.generateInputFiles(files); if (inputFiles == null || inputFiles.size() == 0) { 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 index 192bed0e4..a9e1f4103 100644 --- a/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java +++ b/src/main/java/stirling/software/SPDF/controller/api/pipeline/PipelineDirectoryProcessor.java @@ -17,7 +17,9 @@ import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -34,6 +36,7 @@ import stirling.software.SPDF.config.RuntimePathConfig; import stirling.software.SPDF.model.PipelineConfig; import stirling.software.SPDF.model.PipelineOperation; import stirling.software.SPDF.model.PipelineResult; +import stirling.software.SPDF.service.PostHogService; import stirling.software.SPDF.utils.FileMonitor; @Service @@ -41,15 +44,11 @@ import stirling.software.SPDF.utils.FileMonitor; public class PipelineDirectoryProcessor { private final ObjectMapper objectMapper; - private final ApiDocService apiDocService; - private final PipelineProcessor processor; - private final FileMonitor fileMonitor; - + private final PostHogService postHogService; private final String watchedFoldersDir; - private final String finishedFoldersDir; public PipelineDirectoryProcessor( @@ -57,13 +56,15 @@ public class PipelineDirectoryProcessor { ApiDocService apiDocService, PipelineProcessor processor, FileMonitor fileMonitor, + PostHogService postHogService, RuntimePathConfig runtimePathConfig) { this.objectMapper = objectMapper; this.apiDocService = apiDocService; - this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath(); - this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath(); this.processor = processor; this.fileMonitor = fileMonitor; + this.postHogService = postHogService; + this.watchedFoldersDir = runtimePathConfig.getPipelineWatchedFoldersPath(); + this.finishedFoldersDir = runtimePathConfig.getPipelineFinishedFoldersPath(); } @Scheduled(fixedRate = 60000) @@ -152,6 +153,14 @@ public class PipelineDirectoryProcessor { log.debug("No files detected for {} ", dir); return; } + + List operationNames = + config.getOperations().stream().map(PipelineOperation::getOperation).toList(); + Map properties = new HashMap<>(); + properties.put("operations", operationNames); + properties.put("fileCount", files.length); + postHogService.captureEvent("pipeline_directory_event", properties); + List filesToProcess = prepareFilesForProcessing(files, processingDir); runPipelineAgainstFiles(filesToProcess, config, dir, processingDir); } @@ -252,8 +261,7 @@ public class PipelineDirectoryProcessor { try { Thread.sleep(retryDelayMs * (int) Math.pow(2, attempt - 1)); } catch (InterruptedException e1) { - // TODO Auto-generated catch block - e1.printStackTrace(); + log.error("prepareFilesForProcessing failure", e); } } } 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 34202cff9..ed6b247ce 100644 --- a/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java +++ b/src/main/java/stirling/software/SPDF/controller/web/GeneralWebController.java @@ -13,7 +13,6 @@ import java.util.stream.Stream; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -31,6 +30,7 @@ import stirling.software.SPDF.config.RuntimePathConfig; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.model.SignatureFile; import stirling.software.SPDF.service.SignatureService; +import stirling.software.SPDF.utils.GeneralUtils; @Controller @Tag(name = "General", description = "General APIs") @@ -241,8 +241,7 @@ public class GeneralWebController { private List getFontNamesFromLocation(String locationPattern) { try { Resource[] resources = - ResourcePatternUtils.getResourcePatternResolver(resourceLoader) - .getResources(locationPattern); + GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader); return Arrays.stream(resources) .map( resource -> { 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 3fb15791d..147d163e8 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 @@ -14,7 +14,7 @@ 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"}) + allowableValues = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}) private Integer optimizeLevel; @Schema(description = "The expected output size, e.g. '100MB', '25KB', etc.") diff --git a/src/main/java/stirling/software/SPDF/service/CustomPDFDocumentFactory.java b/src/main/java/stirling/software/SPDF/service/CustomPDFDocumentFactory.java index 354324744..92055a76c 100644 --- a/src/main/java/stirling/software/SPDF/service/CustomPDFDocumentFactory.java +++ b/src/main/java/stirling/software/SPDF/service/CustomPDFDocumentFactory.java @@ -77,7 +77,7 @@ public class CustomPDFDocumentFactory { } long fileSize = file.length(); - log.info("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024)); + log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024)); return loadAdaptively(file, fileSize); } @@ -92,7 +92,7 @@ public class CustomPDFDocumentFactory { } long fileSize = Files.size(path); - log.info("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024)); + log.debug("Loading PDF from file, size: {}MB", fileSize / (1024 * 1024)); return loadAdaptively(path.toFile(), fileSize); } @@ -104,7 +104,7 @@ public class CustomPDFDocumentFactory { } long dataSize = input.length; - log.info("Loading PDF from byte array, size: {}MB", dataSize / (1024 * 1024)); + log.debug("Loading PDF from byte array, size: {}MB", dataSize / (1024 * 1024)); return loadAdaptively(input, dataSize); } @@ -150,7 +150,7 @@ public class CustomPDFDocumentFactory { long actualFreeMemory = maxMemory - usedMemory; // Log memory status - log.info( + log.debug( "Memory status - Free: {}MB ({}%), Used: {}MB, Max: {}MB", actualFreeMemory / (1024 * 1024), String.format("%.2f", freeMemoryPercent), @@ -160,21 +160,21 @@ public class CustomPDFDocumentFactory { // If free memory is critically low, always use file-based caching if (freeMemoryPercent < MIN_FREE_MEMORY_PERCENTAGE || actualFreeMemory < MIN_FREE_MEMORY_BYTES) { - log.info( + log.debug( "Low memory detected ({}%), forcing file-based cache", String.format("%.2f", freeMemoryPercent)); return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly()); } else if (contentSize < SMALL_FILE_THRESHOLD) { - log.info("Using memory-only cache for small document ({}KB)", contentSize / 1024); + log.debug("Using memory-only cache for small document ({}KB)", contentSize / 1024); return IOUtils.createMemoryOnlyStreamCache(); } else if (contentSize < LARGE_FILE_THRESHOLD) { // For medium files (10-50MB), use a mixed approach - log.info( + log.debug( "Using mixed memory/file cache for medium document ({}MB)", contentSize / (1024 * 1024)); return createScratchFileCacheFunction(MemoryUsageSetting.setupMixed(LARGE_FILE_USAGE)); } else { - log.info("Using file-based cache for large document"); + log.debug("Using file-based cache for large document"); return createScratchFileCacheFunction(MemoryUsageSetting.setupTempFileOnly()); } } @@ -237,7 +237,7 @@ public class CustomPDFDocumentFactory { byte[] bytes, long size, StreamCacheCreateFunction cache, String password) throws IOException { if (size >= SMALL_FILE_THRESHOLD) { - log.info("Writing large byte array to temp file for password-protected PDF"); + log.debug("Writing large byte array to temp file for password-protected PDF"); Path tempFile = createTempFile("pdf-bytes-"); Files.write(tempFile, bytes); @@ -261,7 +261,6 @@ public class CustomPDFDocumentFactory { removePassword(doc); } - private PDDocument loadFromFile(File file, long size, StreamCacheCreateFunction cache) throws IOException { return Loader.loadPDF(new DeletingRandomAccessFile(file), "", null, null, cache); @@ -270,7 +269,7 @@ public class CustomPDFDocumentFactory { private PDDocument loadFromBytes(byte[] bytes, long size, StreamCacheCreateFunction cache) throws IOException { if (size >= SMALL_FILE_THRESHOLD) { - log.info("Writing large byte array to temp file"); + log.debug("Writing large byte array to temp file"); Path tempFile = createTempFile("pdf-bytes-"); Files.write(tempFile, bytes); @@ -318,7 +317,7 @@ public class CustomPDFDocumentFactory { // Temp file handling with enhanced logging private Path createTempFile(String prefix) throws IOException { Path file = Files.createTempFile(prefix + tempCounter.incrementAndGet() + "-", ".tmp"); - log.info("Created temp file: {}", file); + log.debug("Created temp file: {}", file); return file; } diff --git a/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java b/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java index ad911f969..1a61d03bd 100644 --- a/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java +++ b/src/main/java/stirling/software/SPDF/service/MetricsAggregatorService.java @@ -4,6 +4,8 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -11,22 +13,32 @@ import org.springframework.stereotype.Service; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.search.Search; +import stirling.software.SPDF.config.EndpointInspector; + @Service public class MetricsAggregatorService { + private static final Logger logger = LoggerFactory.getLogger(MetricsAggregatorService.class); private final MeterRegistry meterRegistry; private final PostHogService postHogService; + private final EndpointInspector endpointInspector; private final Map lastSentMetrics = new ConcurrentHashMap<>(); @Autowired - public MetricsAggregatorService(MeterRegistry meterRegistry, PostHogService postHogService) { + public MetricsAggregatorService( + MeterRegistry meterRegistry, + PostHogService postHogService, + EndpointInspector endpointInspector) { this.meterRegistry = meterRegistry; this.postHogService = postHogService; + this.endpointInspector = endpointInspector; } @Scheduled(fixedRate = 7200000) // Run every 2 hours public void aggregateAndSendMetrics() { Map metrics = new HashMap<>(); + + final boolean validateGetEndpoints = endpointInspector.getValidGetEndpoints().size() != 0; Search.in(meterRegistry) .name("http.requests") .counters() @@ -34,35 +46,52 @@ public class MetricsAggregatorService { counter -> { String method = counter.getId().getTag("method"); String uri = counter.getId().getTag("uri"); - // Skip if either method or uri is null if (method == null || uri == null) { return; } + + // Skip URIs that are 2 characters or shorter + if (uri.length() <= 2) { + return; + } + + // Skip non-GET and non-POST requests if (!"GET".equals(method) && !"POST".equals(method)) { return; } - // Skip URIs that are 2 characters or shorter - if (uri.length() <= 2) { + + // For POST requests, only include if they start with /api/v1 + if ("POST".equals(method) && !uri.contains("api/v1")) { + return; + } + + if (uri.contains(".txt")) { + return; + } + // For GET requests, validate if we have a list of valid endpoints + if ("GET".equals(method) + && validateGetEndpoints + && !endpointInspector.isValidGetEndpoint(uri)) { + logger.debug("Skipping invalid GET endpoint: {}", uri); return; } String key = String.format( "http_requests_%s_%s", method, uri.replace("/", "_")); - double currentCount = counter.count(); double lastCount = lastSentMetrics.getOrDefault(key, 0.0); double difference = currentCount - lastCount; - if (difference > 0) { + logger.info("{}, {}", key, difference); metrics.put(key, difference); lastSentMetrics.put(key, currentCount); } }); - // Send aggregated metrics to PostHog if (!metrics.isEmpty()) { + postHogService.captureEvent("aggregated_metrics", metrics); } } diff --git a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java index d2615935f..9172b5151 100644 --- a/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java +++ b/src/main/java/stirling/software/SPDF/utils/GeneralUtils.java @@ -15,6 +15,9 @@ import java.util.Enumeration; import java.util.List; import java.util.UUID; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.web.multipart.MultipartFile; import com.fathzer.soft.javaluator.DoubleEvaluator; @@ -73,6 +76,19 @@ public class GeneralUtils { return safeName; } + // Get resources from a location pattern + public static Resource[] getResourcesFromLocationPattern( + String locationPattern, ResourceLoader resourceLoader) throws Exception { + // Normalize the path for file resources + if (locationPattern.startsWith("file:")) { + String rawPath = locationPattern.substring(5).replace("\\*", "").replace("/*", ""); + Path normalizePath = Paths.get(rawPath).normalize(); + locationPattern = "file:" + normalizePath.toString().replace("\\", "/") + "/*"; + } + return ResourcePatternUtils.getResourcePatternResolver(resourceLoader) + .getResources(locationPattern); + } + public static boolean isValidURL(String urlStr) { try { Urls.create( diff --git a/src/main/resources/messages_de_DE.properties b/src/main/resources/messages_de_DE.properties index 6d73e5dca..7a01d327d 100644 --- a/src/main/resources/messages_de_DE.properties +++ b/src/main/resources/messages_de_DE.properties @@ -97,7 +97,7 @@ pipeline.header=Pipeline-Menü (Beta) pipeline.uploadButton=Benutzerdefinierter Upload pipeline.configureButton=Konfigurieren pipeline.defaultOption=Benutzerdefiniert -pipeline.submitButton=Speichern +pipeline.submitButton=Ausführen pipeline.help=Hilfe für Pipeline pipeline.scanHelp=Hilfe zum Ordnerscan pipeline.deletePrompt=Möchten Sie die Pipeline wirklich löschen? @@ -262,7 +262,7 @@ home.desc=Ihr lokal gehosteter One-Stop-Shop für alle Ihre PDF-Anforderungen. home.searchBar=Suche nach Funktionen... -home.viewPdf.title=View/Edit PDF +home.viewPdf.title=PDF anzeigen/bearbeiten home.viewPdf.desc=Anzeigen, Kommentieren, Text oder Bilder hinzufügen viewPdf.tags=anzeigen,lesen,kommentieren,text,bild @@ -273,7 +273,7 @@ home.legacyHomepage=Alte Homepage home.newHomePage=Probieren Sie unsere neue Homepage aus! home.alphabetical=Alphabetisch home.globalPopularity=Beliebtheit -home.sortBy=Sort by: +home.sortBy=Sortieren nach: home.multiTool.title=PDF-Multitool home.multiTool.desc=Seiten zusammenführen, drehen, neu anordnen und entfernen @@ -615,7 +615,7 @@ redact.showAttatchments=Zeige Anhänge redact.showLayers=Ebenen anzeigen (Doppelklick, um alle Ebenen auf den Standardzustand zurückzusetzen) redact.colourPicker=Farbauswahl redact.findCurrentOutlineItem=Aktuell gewähltes Element finden -redact.applyChanges=Apply Changes +redact.applyChanges=Änderungen übernehmen #showJS showJS.title=Javascript anzeigen diff --git a/src/main/resources/messages_fr_FR.properties b/src/main/resources/messages_fr_FR.properties index ac6a78b27..e5500ffc0 100644 --- a/src/main/resources/messages_fr_FR.properties +++ b/src/main/resources/messages_fr_FR.properties @@ -860,8 +860,8 @@ sign.last=Dernière page sign.next=Page suivante sign.previous=Page précédente sign.maintainRatio=Conserver les proportions -sign.undo=Undo -sign.redo=Redo +sign.undo=Défaire +sign.redo=Refaire #repair repair.title=Réparer @@ -1281,15 +1281,15 @@ survey.please=Veuillez envisager de répondre à notre enquête ! survey.disabled=(La fenêtre contextuelle de l'enquête sera désactivée dans les mises à jour suivantes mais sera disponible en bas de page) survey.button=Répondre à l'enquête survey.dontShowAgain=Ne plus afficher -survey.meeting.1=If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session. -survey.meeting.2=This is a chance to: -survey.meeting.3=Get help with deployment, integrations, or troubleshooting -survey.meeting.4=Provide direct feedback on performance, edge cases, and feature gaps -survey.meeting.5=Help us refine Stirling PDF for real-world enterprise use -survey.meeting.6=If you're interested, you can book time with our team directly. (English speaking only) -survey.meeting.7=Looking forward to digging into your use cases and making Stirling PDF even better! -survey.meeting.notInterested=Not a business and/or interested in a meeting? -survey.meeting.button=Book meeting +survey.meeting.1=Si vous utilisez Stirling PDF au travail, nous aimerions en discuter avec vous. Nous offrons des sessions de support technique en échante d'une discussion de 15 minutes pour découvrir nos utilisateurs. +survey.meeting.2=C'est l'occasion de : +survey.meeting.3=Obtenir de l'aide pour le déploiement, l'intégration ou résoudre des problèmes +survey.meeting.4=Fournir un retour direct sur les performances, les cas limites, les fonctionnalités demandées +survey.meeting.5=Nous aider à adapter Stirling PDF aux usages réels en entreprise +survey.meeting.6=Si vous êtes intéressé, prenez rendez-vous avec notre équipe (en anglias uniquement) +survey.meeting.7=Nous avons hâte de découvrir vos cas d'usage et d'améliorer encore Stirling PDF ! +survey.meeting.notInterested=Bous n'êtes pas une entreprise et/ou n'êtes pas intéressé par une discussion ? +survey.meeting.button=Prendre rendez-vous #error error.sorry=Désolé pour ce problème ! diff --git a/src/main/resources/messages_pt_BR.properties b/src/main/resources/messages_pt_BR.properties index f66dc54a1..483cf54a5 100644 --- a/src/main/resources/messages_pt_BR.properties +++ b/src/main/resources/messages_pt_BR.properties @@ -262,15 +262,15 @@ home.desc=Seu tudo-em-um hospedado localmente para tudo relacionado a PDFs home.searchBar=Pesquisar funcionalidades... -home.viewPdf.title=View/Edit PDF +home.viewPdf.title=Ver/Editar PDF home.viewPdf.desc=Visualizar, anotar, adicionar texto ou imagens ao PDF. viewPdf.tags=visualizar,ler,anotar,texto,imagem home.setFavorites=Adicionar Favoritos home.hideFavorites=Ocultar Favoritos home.showFavorites=Mostrar Favoritos -home.legacyHomepage=Homepage Antiga -home.newHomePage=Experimente nossa nova Homepage! +home.legacyHomepage=Página Inicial Antiga +home.newHomePage=Experimente nossa nova Página Inicial! home.alphabetical=Alfabética home.globalPopularity=Popularidade Global home.sortBy=Ordenar por: @@ -615,7 +615,7 @@ redact.showAttatchments=Mostrar Anexos redact.showLayers=Mostrar Camadas (duplo clique para restabelecer as camadas para o estado padrão) redact.colourPicker=Seletor de Cores redact.findCurrentOutlineItem=Encontrar item atual -redact.applyChanges=Apply Changes +redact.applyChanges=Aplicar Alterações #showJS showJS.title=Mostrar JavaScript @@ -860,8 +860,8 @@ sign.last=Última página sign.next=Próxima página sign.previous=Página anterior sign.maintainRatio=Habilitar manter proporção -sign.undo=Undo -sign.redo=Redo +sign.undo=Desfazer +sign.redo=Refazer #repair repair.title=Reparar @@ -932,8 +932,8 @@ compress.title=Comprimir compress.header=Comprimir compress.credit=Este serviço usa o Qpdf para compressão/otimização de PDF. compress.grayscale.label=Aplicar escala de cinza para compressão -compress.selectText.1=Compression Settings -compress.selectText.1.1=1-3 PDF compression,
4-6 lite image compression,
7-9 intense image compression Will dramatically reduce image quality +compress.selectText.1=Configurações de Compressão: +compress.selectText.1.1=1-3: Compressão do PDF,
4-6: Compressão leve de Imagem,
7-9: Compressão alta de Imagem. Redução considerável de qualidade da imagem. compress.selectText.2=Nível de Otimização: compress.selectText.4=Modo Automático - Ajusta automaticamente a qualidade para atingir o tamanho exato desejado compress.selectText.5=Tamanho esperado do PDF (por exemplo, 25 MB, 10,8 MB, 25 KB): @@ -972,7 +972,7 @@ pdfOrganiser.mode.7=Remover primeiro pdfOrganiser.mode.8=Remover último pdfOrganiser.mode.9=Remover o primeiro e o último pdfOrganiser.mode.10=Mesclagem ímpar-par -pdfOrganiser.mode.11=Duplicate all pages +pdfOrganiser.mode.11=Duplicar todas as páginas pdfOrganiser.placeholder=(por exemplo 1,3,2 ou 4-8,2,10-12 ou 2n-1) @@ -1015,7 +1015,7 @@ decrypt.success=File decrypted successfully. multiTool-advert.message=Esta função também está disponível em Multiferramentas de PDF. Com uma interface mais completa e funções adicionais. #view pdf -viewPdf.title=View/Edit PDF +viewPdf.title=Ver/Editar PDF viewPdf.header=Visualizar PDF #pageRemover @@ -1281,15 +1281,15 @@ survey.please=Por favor, considere responder à nossa pesquisa! survey.disabled=(O pop-up da pesquisa será desativado nas atualizações seguintes, mas estará disponível no rodapé da página) survey.button=Responder a Pesquisa survey.dontShowAgain=Não mostre novamente. -survey.meeting.1=If you're using Stirling PDF at work, we'd love to speak to you. We're offering technical support sessions in exchange for a 15 minute user discovery session. -survey.meeting.2=This is a chance to: -survey.meeting.3=Get help with deployment, integrations, or troubleshooting -survey.meeting.4=Provide direct feedback on performance, edge cases, and feature gaps -survey.meeting.5=Help us refine Stirling PDF for real-world enterprise use -survey.meeting.6=If you're interested, you can book time with our team directly. (English speaking only) -survey.meeting.7=Looking forward to digging into your use cases and making Stirling PDF even better! -survey.meeting.notInterested=Not a business and/or interested in a meeting? -survey.meeting.button=Book meeting +survey.meeting.1=Se você está utilizando o Stirling PDF em ambiente empresarial, nos vamos amar falar com você. Nós estamos oferecendo sessões de suporte técnico em troca de uma sessão de descoberta de usuários de 15 minutos. +survey.meeting.2=Essa é uma chance para: +survey.meeting.3=Obter ajuda com implementação, integração ou resolução de problemas +survey.meeting.4=Prover feedback sobre desempenho, casos especiais e lacunas de funcionalidades +survey.meeting.5=Nos ajude a melhorar o Stirling PDF para uso empresarial no mundo real +survey.meeting.6=Se você está interessado, você pode agendar um horário com nosso time diretamente. (Apenas em Inglês) +survey.meeting.7=Estamos ansiosos para entender seu uso do software e tornar o Stirling PDF ainda melhor! +survey.meeting.notInterested=Não é uma empresa e/ou não tem interesse em uma reunião? +survey.meeting.button=Agendar Reunião #error error.sorry=Desculpe pelo problema! diff --git a/src/main/resources/static/3rdPartyLicenses.json b/src/main/resources/static/3rdPartyLicenses.json index 2101765d9..62019e135 100644 --- a/src/main/resources/static/3rdPartyLicenses.json +++ b/src/main/resources/static/3rdPartyLicenses.json @@ -3,14 +3,14 @@ { "moduleName": "ch.qos.logback:logback-classic", "moduleUrl": "http://www.qos.ch", - "moduleVersion": "1.5.17", + "moduleVersion": "1.5.18", "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, { "moduleName": "ch.qos.logback:logback-core", "moduleUrl": "http://www.qos.ch", - "moduleVersion": "1.5.17", + "moduleVersion": "1.5.18", "moduleLicense": "GNU Lesser General Public License", "moduleLicenseUrl": "http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html" }, @@ -45,77 +45,77 @@ { "moduleName": "com.fasterxml.jackson.core:jackson-annotations", "moduleUrl": "https://github.com/FasterXML/jackson", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.core:jackson-core", "moduleUrl": "https://github.com/FasterXML/jackson-core", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.core:jackson-databind", "moduleUrl": "https://github.com/FasterXML/jackson", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", "moduleUrl": "https://github.com/FasterXML/jackson-dataformats-text", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.datatype:jackson-datatype-jdk8", "moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jdk8", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", "moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-datatype-jsr310", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.jaxrs:jackson-jaxrs-base", "moduleUrl": "https://github.com/FasterXML/jackson-jaxrs-providers/jackson-jaxrs-base", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.jaxrs:jackson-jaxrs-json-provider", "moduleUrl": "https://github.com/FasterXML/jackson-jaxrs-providers/jackson-jaxrs-json-provider", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.module:jackson-module-jaxb-annotations", "moduleUrl": "https://github.com/FasterXML/jackson-modules-base", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson.module:jackson-module-parameter-names", "moduleUrl": "https://github.com/FasterXML/jackson-modules-java8/jackson-module-parameter-names", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "com.fasterxml.jackson:jackson-bom", "moduleUrl": "https://github.com/FasterXML/jackson-bom", - "moduleVersion": "2.18.2", + "moduleVersion": "2.18.3", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -546,7 +546,7 @@ { "moduleName": "io.micrometer:micrometer-commons", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.14.4", + "moduleVersion": "1.14.5", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -560,14 +560,14 @@ { "moduleName": "io.micrometer:micrometer-jakarta9", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.14.4", + "moduleVersion": "1.14.5", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, { "moduleName": "io.micrometer:micrometer-observation", "moduleUrl": "https://github.com/micrometer-metrics/micrometer", - "moduleVersion": "1.14.4", + "moduleVersion": "1.14.5", "moduleLicense": "The Apache Software License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -876,7 +876,7 @@ { "moduleName": "org.apache.tomcat.embed:tomcat-embed-el", "moduleUrl": "https://tomcat.apache.org/", - "moduleVersion": "10.1.36", + "moduleVersion": "10.1.39", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt" }, @@ -903,7 +903,7 @@ { "moduleName": "org.aspectj:aspectjweaver", "moduleUrl": "https://www.eclipse.org/aspectj/", - "moduleVersion": "1.9.22.1", + "moduleVersion": "1.9.23", "moduleLicense": "Eclipse Public License - v 2.0", "moduleLicenseUrl": "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt" }, @@ -971,182 +971,182 @@ { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jetty-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-servlet", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-annotations", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-plus", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlet", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-servlets", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.ee10:jetty-ee10-webapp", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-core-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-api", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty.websocket:jetty-websocket-jetty-common", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-alpn-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-client", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-ee", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-http", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-io", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-plus", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-security", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-server", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-session", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-util", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, { "moduleName": "org.eclipse.jetty:jetty-xml", "moduleUrl": "https://jetty.org/", - "moduleVersion": "12.0.16", + "moduleVersion": "12.0.18", "moduleLicense": "Eclipse Public License - Version 2.0", "moduleLicenseUrl": "https://www.eclipse.org/legal/epl-2.0/" }, @@ -1188,7 +1188,7 @@ { "moduleName": "org.hibernate.orm:hibernate-core", "moduleUrl": "https://www.hibernate.org/orm/6.6", - "moduleVersion": "6.6.8.Final", + "moduleVersion": "6.6.11.Final", "moduleLicense": "GNU Library General Public License v2.1 or later", "moduleLicenseUrl": "https://www.opensource.org/licenses/LGPL-2.1" }, @@ -1353,16 +1353,16 @@ { "moduleName": "org.slf4j:jul-to-slf4j", "moduleUrl": "http://www.slf4j.org", - "moduleVersion": "2.0.16", - "moduleLicense": "MIT License", - "moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php" + "moduleVersion": "2.0.17", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/license/mit" }, { "moduleName": "org.slf4j:slf4j-api", "moduleUrl": "http://www.slf4j.org", - "moduleVersion": "2.0.16", - "moduleLicense": "MIT License", - "moduleLicenseUrl": "http://www.opensource.org/licenses/mit-license.php" + "moduleVersion": "2.0.17", + "moduleLicense": "MIT", + "moduleLicenseUrl": "https://opensource.org/license/mit" }, { "moduleName": "org.snakeyaml:snakeyaml-engine", @@ -1392,182 +1392,182 @@ { "moduleName": "org.springframework.boot:spring-boot", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-actuator", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-actuator-autoconfigure", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-autoconfigure", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-devtools", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-actuator", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-data-jpa", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-jdbc", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-jetty", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-json", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-logging", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-oauth2-client", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-security", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-thymeleaf", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.boot:spring-boot-starter-web", "moduleUrl": "https://spring.io/projects/spring-boot", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.data:spring-data-commons", "moduleUrl": "https://spring.io/projects/spring-data", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.data:spring-data-jpa", "moduleUrl": "https://projects.spring.io/spring-data-jpa", - "moduleVersion": "3.4.3", + "moduleVersion": "3.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-config", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-core", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-crypto", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-client", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-core", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-oauth2-jose", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-saml2-service-provider", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework.security:spring-security-web", "moduleUrl": "https://spring.io/projects/spring-security", - "moduleVersion": "6.4.3", + "moduleVersion": "6.4.4", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, @@ -1581,84 +1581,84 @@ { "moduleName": "org.springframework:spring-aop", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-aspects", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-beans", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-context", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-core", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-expression", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-jcl", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-jdbc", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-orm", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-tx", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-web", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, { "moduleName": "org.springframework:spring-webmvc", "moduleUrl": "https://github.com/spring-projects/spring-framework", - "moduleVersion": "6.2.3", + "moduleVersion": "6.2.5", "moduleLicense": "Apache License, Version 2.0", "moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0" }, diff --git a/src/main/resources/static/css/multi-tool.css b/src/main/resources/static/css/multi-tool.css index 99e15f231..a540b9d0a 100644 --- a/src/main/resources/static/css/multi-tool.css +++ b/src/main/resources/static/css/multi-tool.css @@ -14,26 +14,30 @@ label { border-radius: 16px !important; padding: 0.75rem; border: 1px solid var(--theme-color-outline-variant); + flex-grow: 5; } .mt-action-bar { display: flex; gap: 10px; align-items: start; - background-color: var(--md-sys-color-surface-5); border: none; backdrop-filter: blur(2px); top: 10px; - z-index: 10; + z-index: 11; padding: 1.25rem; border-radius: 2rem; margin: 0px 25px; + justify-content:center; } + .mt-action-bar>* { padding-bottom: 0.5rem; } - +.mt-file-uploader { + width:100% +} .mt-action-bar svg, .mt-action-btn svg { width: 20px; @@ -42,21 +46,29 @@ label { .mt-action-bar .mt-filename { width: 100%; + display: flex; + gap: 10px; } .mt-action-btn { + position: sticky; + bottom: 10%; + margin: auto; + margin-bottom: 25px; + border-radius: 2rem; + z-index: 12; + background-color: var(--md-sys-color-surface-container-low) ; display: flex; gap: 10px; - align-items: start; - top: 10px; - z-index: 10; padding: 12px 0px 0px; - width: 100%; + width: fit-content; + justify-content: center; + padding: 10px 20px } .mt-action-btn .btn { - width: 3rem; - height: 3rem; + width: 3.5rem; + height: 3.5rem; border-radius: 20px; padding: 0; } @@ -64,7 +76,7 @@ label { .bg-card { background-color: var(--md-sys-color-surface-5); border-radius: 3rem; - padding: 25px 0 0; + padding: 25px 0; } #pages-container-wrapper { @@ -73,7 +85,7 @@ label { flex-direction: column; padding: 1rem; border-radius: 25px; - overflow-y: auto; + overflow-y: clip; overflow-x: auto; min-height: 275px; margin: 0 0 30px 0; @@ -136,10 +148,6 @@ label { display: none; } -/* Pushes the last item to the left */ -.page-container:last-child { - margin-right: auto; -} .page-container:last-child:lang(ar), /* Arabic */ diff --git a/src/main/resources/static/css/theme/componentes.css b/src/main/resources/static/css/theme/componentes.css index 22e868912..c10806ff7 100644 --- a/src/main/resources/static/css/theme/componentes.css +++ b/src/main/resources/static/css/theme/componentes.css @@ -39,6 +39,10 @@ textarea { border: 5px solid var(--md-sys-color-surface-5); } +*::-webkit-scrollbar-corner { + background-color: var(--md-sys-color-surface); +} + /* Alerts */ .alert { border-radius: 3rem; @@ -877,6 +881,7 @@ textarea.form-control { margin: 0 1%; padding: 1.5rem 0; border-radius: 1rem; + min-width: 20rem; color: var(--md-sys-color-on-surface); background-color: var(--md-sys-color-surface-container); border: 1px solid var(--md-sys-color-surface-5); diff --git a/src/main/resources/static/js/DecryptFiles.js b/src/main/resources/static/js/DecryptFiles.js index 8c5b8a50e..6701649c3 100644 --- a/src/main/resources/static/js/DecryptFiles.js +++ b/src/main/resources/static/js/DecryptFiles.js @@ -3,7 +3,7 @@ export class DecryptFile { constructor(){ this.decryptWorker = null } - + async decryptFile(file, requiresPassword) { try { @@ -87,7 +87,7 @@ export class DecryptFile { } pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; - + const arrayBuffer = await file.arrayBuffer(); const arrayBufferForPdfLib = arrayBuffer.slice(0); var loadingTask; @@ -98,7 +98,7 @@ export class DecryptFile { }); this.decryptWorker = loadingTask._worker - }else { + }else { loadingTask = pdfjsLib.getDocument({ data: arrayBuffer, worker: this.decryptWorker diff --git a/src/main/resources/static/js/downloader.js b/src/main/resources/static/js/downloader.js index 77a673c04..900e2539a 100644 --- a/src/main/resources/static/js/downloader.js +++ b/src/main/resources/static/js/downloader.js @@ -130,7 +130,7 @@ async function getPDFPageCount(file) { try { const arrayBuffer = await file.arrayBuffer(); - pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs'; + pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; const pdf = await pdfjsLib.getDocument({data: arrayBuffer}).promise; return pdf.numPages; } catch (error) { diff --git a/src/main/resources/static/js/fileInput.js b/src/main/resources/static/js/fileInput.js index 58eaffd4b..5fc61e8af 100644 --- a/src/main/resources/static/js/fileInput.js +++ b/src/main/resources/static/js/fileInput.js @@ -34,6 +34,7 @@ function setupFileInput(chooser) { const filesSelected = chooser.getAttribute('data-bs-files-selected'); const pdfPrompt = chooser.getAttribute('data-bs-pdf-prompt'); const inputContainerId = chooser.getAttribute('data-bs-element-container-id'); + const showUploads = chooser.getAttribute('data-bs-show-uploads') === "true"; let inputContainer = document.getElementById(inputContainerId); let fileInput = document.getElementById(elementId); @@ -47,6 +48,11 @@ function setupFileInput(chooser) { let overlay; let dragCounter = 0; + input.addEventListener('reset', (e) => { + allFiles = []; + input.value = null; + }); + inputContainer.addEventListener('click', (e) => { let inputBtn = document.getElementById(elementId); inputBtn.click(); @@ -56,7 +62,6 @@ function setupFileInput(chooser) { fileInput.addEventListener("invalid", (e) => { e.preventDefault(); alert(pdfPrompt); - console.log(escapeHtml(translations.selectPDF)); }); const dragenterListener = function () { @@ -142,7 +147,17 @@ function setupFileInput(chooser) { allFiles = Array.from(isDragAndDrop ? allFiles : [element.files[0]]); } + const originalText = inputContainer.querySelector('#fileInputText').innerHTML; + + inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.loading; + async function checkZipFile() { + const hasZipFiles = allFiles.some(file => zipTypes.includes(file.type)); + + // Only change to extractPDF message if we actually have zip files + if (hasZipFiles) { + inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.extractPDF; + } const promises = allFiles.map(async (file, index) => { try { @@ -157,12 +172,9 @@ function setupFileInput(chooser) { }); await Promise.all(promises); - } - const originalText = inputContainer.querySelector('#fileInputText').innerHTML; - const decryptFile = new DecryptFile(); - inputContainer.querySelector('#fileInputText').innerHTML = window.fileInput.extractPDF; + const decryptFile = new DecryptFile(); await checkZipFile(); @@ -225,26 +237,26 @@ function setupFileInput(chooser) { .then(function (zip) { var extractionPromises = []; - zip.forEach(function (relativePath, zipEntry) { + zip.forEach(function (relativePath, zipEntry) { + const promise = zipEntry.async('blob').then(function (content) { + // Assuming that folders have size zero + if (content.size > 0) { + const extension = zipEntry.name.split('.').pop().toLowerCase(); + const mimeType = mimeTypes[extension] || 'application/octet-stream'; - const promise = zipEntry.async('blob').then(function (content) { - // Assuming that folders have size zero - if (content.size > 0) { - const extension = zipEntry.name.split('.').pop().toLowerCase(); - const mimeType = mimeTypes[extension]; - - // Check for file extension - if (mimeType && (mimeType.startsWith(acceptedFileType.split('/')[0]) || acceptedFileType === mimeType)) { - - var file = new File([content], zipEntry.name, { type: mimeType }); - file.uniqueId = UUID.uuidv4(); - allFiles.push(file); - - } else { - console.log(`File ${zipEntry.name} skipped. MIME type (${mimeType}) does not match accepted type (${acceptedFileType})`); - } - } - }); + // Check if we're accepting ONLY ZIP files (in which case extract everything) + // or if the file type matches the accepted type + if (zipTypes.includes(acceptedFileType) || + acceptedFileType === '*/*' || + (mimeType && (mimeType.startsWith(acceptedFileType.split('/')[0]) || acceptedFileType === mimeType))) { + var file = new File([content], zipEntry.name, { type: mimeType }); + file.uniqueId = UUID.uuidv4(); + allFiles.push(file); + } else { + console.log(`File ${zipEntry.name} skipped. MIME type (${mimeType}) does not match accepted type (${acceptedFileType})`); + } + } + }); extractionPromises.push(promise); }); @@ -361,7 +373,7 @@ function setupFileInput(chooser) { } function showOrHideSelectedFilesContainer(files) { - if (files && files.length > 0) { + if (showUploads && files && files.length > 0) { chooser.style.setProperty('--selected-files-display', 'flex'); } else { chooser.style.setProperty('--selected-files-display', 'none'); diff --git a/src/main/resources/static/js/multitool/PdfContainer.js b/src/main/resources/static/js/multitool/PdfContainer.js index f9955a71c..72dd7f4c0 100644 --- a/src/main/resources/static/js/multitool/PdfContainer.js +++ b/src/main/resources/static/js/multitool/PdfContainer.js @@ -72,6 +72,7 @@ class PdfContainer { window.addFilesBlankAll = this.addFilesBlankAll; window.removeAllElements = this.removeAllElements; window.resetPages = this.resetPages; + window.selectAll = false; let undoBtn = document.getElementById('undo-btn'); let redoBtn = document.getElementById('redo-btn'); @@ -129,6 +130,10 @@ class PdfContainer { return commandSequence; } + showButton(button, show) { + button.classList.toggle('hidden', !show); + } + movePageTo(startElements, endElement, scrollTo = false) { if (Array.isArray(startElements)){ @@ -176,8 +181,10 @@ class PdfContainer { if (files.length > 0) { pages = await this.addFilesFromFiles(files, nextSiblingElement, pages); this.updateFilename(files[0].name); - const selectAll = document.getElementById('select-pages-container'); - selectAll.classList.toggle('hidden', false); + + if(window.selectPage){ + this.showButton(document.getElementById('select-pages-container'), true); + } } resolve(pages); }; @@ -191,9 +198,8 @@ class PdfContainer { const pages = await this.addFilesFromFiles(files, nextSiblingElement, []); this.updateFilename(files[0]?.name || 'untitled'); - const selectAll = document.getElementById('select-pages-container'); - if (selectAll) { - selectAll.classList.remove('hidden'); + if(window.selectPage) { + this.showButton(document.getElementById('select-pages-container'), true); } return pages; @@ -433,12 +439,12 @@ class PdfContainer { const selectIcon = document.getElementById('select-All-Container'); const deselectIcon = document.getElementById('deselect-All-Container'); - if (selectIcon.style.display === 'none') { - selectIcon.style.display = 'inline'; - deselectIcon.style.display = 'none'; + if (!window.selectAll) { + this.showButton(selectIcon, true); + this.showButton(deselectIcon, false); } else { - selectIcon.style.display = 'none'; - deselectIcon.style.display = 'inline'; + this.showButton(selectIcon, false); + this.showButton(deselectIcon, true); } checkboxes.forEach((checkbox) => { checkbox.checked = window.selectAll; @@ -846,8 +852,20 @@ class PdfContainer { deleteButton.classList.toggle('hidden', !window.selectPage); const selectedPages = document.getElementById('selected-pages-display'); selectedPages.classList.toggle('hidden', !window.selectPage); - const selectAll = document.getElementById('select-All-Container'); - selectAll.classList.toggle('hidden', !window.selectPage); + if(!window.selectPage) + { + this.showButton(document.getElementById('deselect-All-Container'), false); + this.showButton(document.getElementById('select-All-Container'), false); + } + else if(window.selectAll){ + this.showButton(document.getElementById('deselect-All-Container'), true); + this.showButton(document.getElementById('select-All-Container'), false); + } + else{ + this.showButton(document.getElementById('deselect-All-Container'), false); + this.showButton(document.getElementById('select-All-Container'), true); + } + const exportSelected = document.getElementById('export-selected-button'); exportSelected.classList.toggle('hidden', !window.selectPage); const selectPagesButton = document.getElementById('select-pages-button'); diff --git a/src/main/resources/static/js/multitool/fileInput.js b/src/main/resources/static/js/multitool/fileInput.js deleted file mode 100644 index ec7fa4c1e..000000000 --- a/src/main/resources/static/js/multitool/fileInput.js +++ /dev/null @@ -1,114 +0,0 @@ -class FileDragManager { - overlay; - dragCounter; - updateFilename; - - constructor(cb = null) { - this.dragCounter = 0; - this.setCallback(cb); - - // Prevent default behavior for drag events - ['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => { - document.body.addEventListener(eventName, preventDefaults, false); - }); - - function preventDefaults(e) { - e.preventDefault(); - e.stopPropagation(); - } - - this.dragenterListener = this.dragenterListener.bind(this); - this.dragleaveListener = this.dragleaveListener.bind(this); - this.dropListener = this.dropListener.bind(this); - - document.body.addEventListener('dragenter', this.dragenterListener); - document.body.addEventListener('dragleave', this.dragleaveListener); - // Add drop event listener - document.body.addEventListener('drop', this.dropListener); - } - - setActions({updateFilename}) { - this.updateFilename = updateFilename; - } - - setCallback(cb) { - if (cb) { - this.callback = cb; - } else { - this.callback = (files) => console.warn('FileDragManager not set'); - } - } - - dragenterListener() { - this.dragCounter++; - if (!this.overlay) { - // Create and show the overlay - this.overlay = document.createElement('div'); - this.overlay.style.position = 'fixed'; - this.overlay.style.top = 0; - this.overlay.style.left = 0; - this.overlay.style.width = '100%'; - this.overlay.style.height = '100%'; - this.overlay.style.background = 'rgba(0, 0, 0, 0.5)'; - this.overlay.style.color = '#fff'; - this.overlay.style.zIndex = '1000'; - this.overlay.style.display = 'flex'; - this.overlay.style.alignItems = 'center'; - this.overlay.style.justifyContent = 'center'; - this.overlay.style.pointerEvents = 'none'; - this.overlay.innerHTML = '

Drop files anywhere to upload

'; - document.getElementById('content-wrap').appendChild(this.overlay); - } - } - - dragleaveListener() { - this.dragCounter--; - if (this.dragCounter === 0) { - // Hide and remove the overlay - if (this.overlay) { - this.overlay.remove(); - this.overlay = null; - } - } - } - - dropListener(e) { - const dt = e.dataTransfer; - const files = dt.files; - this.callback(files) - .catch((err) => { - console.error(err); - //maybe - }) - .finally(() => { - // Hide and remove the overlay - if (this.overlay) { - this.overlay.remove(); - this.overlay = null; - } - - this.updateFilename(files ? files[0].name : ''); - }); - } - - async addImageFile(file, nextSiblingElement) { - const div = document.createElement('div'); - div.classList.add('page-container'); - - var img = document.createElement('img'); - img.classList.add('page-image'); - img.src = URL.createObjectURL(file); - div.appendChild(img); - - this.pdfAdapters.forEach((adapter) => { - adapter.adapt?.(div); - }); - if (nextSiblingElement) { - this.pagesContainer.insertBefore(div, nextSiblingElement); - } else { - this.pagesContainer.appendChild(div); - } - } -} - -export default FileDragManager; diff --git a/src/main/resources/static/js/pages/home.js b/src/main/resources/static/js/pages/home.js index 6e72cdece..8565c44a2 100644 --- a/src/main/resources/static/js/pages/home.js +++ b/src/main/resources/static/js/pages/home.js @@ -33,10 +33,11 @@ function setAnalytics(enabled) { } updateFavoriteIcons(); +const contentPath = /*[[${@contextPath}]]*/ ''; const defaultView = localStorage.getItem('defaultView') || 'home'; // Default to "home" if (defaultView === 'home-legacy') { - window.location.href = '/home-legacy'; // Redirect to legacy view + window.location.href = contentPath + 'home-legacy'; // Redirect to legacy view } document.addEventListener('DOMContentLoaded', function () { diff --git a/src/main/resources/static/js/search.js b/src/main/resources/static/js/search.js index 85d69f846..c7932965c 100644 --- a/src/main/resources/static/js/search.js +++ b/src/main/resources/static/js/search.js @@ -25,51 +25,56 @@ window.onload = function () { window.navItemMaxWidth = maxWidth; }; -// Show search results as user types in search box document.querySelector("#navbarSearchInput").addEventListener("input", function (e) { - var searchText = e.target.value.trim().toLowerCase(); // Trim whitespace and convert to lowercase - var items = document.querySelectorAll('a.dropdown-item[data-bs-tags]'); + var searchText = e.target.value.trim().toLowerCase(); + var items = document.querySelectorAll("a.dropdown-item[data-bs-tags]"); var resultsBox = document.querySelector("#searchResults"); - // Clear any previous results resultsBox.innerHTML = ""; + if (searchText !== "") { - items.forEach(function (item) { - var titleElement = item.querySelector(".icon-text"); - var iconElement = item.querySelector(".material-symbols-rounded, .icon"); - var itemHref = item.getAttribute("href"); - var tags = item.getAttribute("data-bs-tags") || ""; // If no tags, default to empty string + var addedResults = new Set(); + + items.forEach(function (item) { + var titleElement = item.querySelector(".icon-text"); + var iconElement = item.querySelector(".material-symbols-rounded, .icon"); + var itemHref = item.getAttribute("href"); + var tags = item.getAttribute("data-bs-tags") || ""; if (titleElement && iconElement && itemHref !== "#") { - var title = titleElement.innerText; + var title = titleElement.innerText.trim(); + if ( - (title.toLowerCase().indexOf(searchText) !== -1 || tags.toLowerCase().indexOf(searchText) !== -1) && - !resultsBox.querySelector(`a[href="${itemHref}"]`) + (title.toLowerCase().includes(searchText) || tags.toLowerCase().includes(searchText)) && + !addedResults.has(itemHref) ) { - var result = document.createElement("a"); - result.href = itemHref; - result.classList.add("dropdown-item"); + var dropdownItem = document.createElement("div"); + dropdownItem.className = "dropdown-item d-flex justify-content-between align-items-center"; - var resultIcon = document.createElement("span"); - resultIcon.classList.add("material-symbols-rounded"); - resultIcon.textContent = iconElement.textContent; - result.appendChild(resultIcon); + var contentWrapper = document.createElement("div"); + contentWrapper.className = "d-flex align-items-center flex-grow-1"; + contentWrapper.style.textDecoration = "none"; + contentWrapper.style.color = "inherit"; - var resultText = document.createElement("span"); - resultText.textContent = title; - resultText.classList.add("icon-text"); - result.appendChild(resultText); + var originalContent = item.querySelector("div").cloneNode(true); + contentWrapper.appendChild(originalContent); - resultsBox.appendChild(result); + contentWrapper.onclick = function () { + window.location.href = itemHref; + }; + + dropdownItem.appendChild(contentWrapper); + resultsBox.appendChild(dropdownItem); + addedResults.add(itemHref); } } }); } - // Set the width of the search results box to the maximum width resultsBox.style.width = window.navItemMaxWidth + "px"; }); + const searchDropdown = document.getElementById('searchDropdown'); const searchInput = document.getElementById('navbarSearchInput'); const dropdownMenu = searchDropdown.querySelector('.dropdown-menu'); diff --git a/src/main/resources/templates/fragments/common.html b/src/main/resources/templates/fragments/common.html index 1e801d06f..66fa94b58 100644 --- a/src/main/resources/templates/fragments/common.html +++ b/src/main/resources/templates/fragments/common.html @@ -196,7 +196,7 @@ + th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, showUploads=${showUploads} ?: true, notRequired=${notRequired} ?: false">
+ th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-show-uploads=${showUploads}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
diff --git a/src/main/resources/templates/fragments/navbar.html b/src/main/resources/templates/fragments/navbar.html index 8720b2f72..06defd47b 100644 --- a/src/main/resources/templates/fragments/navbar.html +++ b/src/main/resources/templates/fragments/navbar.html @@ -24,7 +24,7 @@ border-bottom-width: 1px; border-color: var(--md-nav-color-on-seperator)">
- + icon @@ -265,17 +265,18 @@
- diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html index 1450c7489..925aefe8e 100644 --- a/src/main/resources/templates/home.html +++ b/src/main/resources/templates/home.html @@ -219,7 +219,7 @@ window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false; /*]]>*/ - + diff --git a/src/main/resources/templates/merge-pdfs.html b/src/main/resources/templates/merge-pdfs.html index f6ace6d74..d10b65801 100644 --- a/src/main/resources/templates/merge-pdfs.html +++ b/src/main/resources/templates/merge-pdfs.html @@ -52,8 +52,8 @@ const pagesTranslation = document.getElementById('pagesTranslation').innerText; // Get translation for multiple pages
diff --git a/src/main/resources/templates/multi-tool.html b/src/main/resources/templates/multi-tool.html index 19ced3d84..3c6c60fba 100644 --- a/src/main/resources/templates/multi-tool.html +++ b/src/main/resources/templates/multi-tool.html @@ -14,117 +14,113 @@

+
construction
-
-
- - -
-
- - - - - - - - - - - - - -
- -
-
- - -
-
-
@@ -180,8 +176,14 @@ import DragDropManager from "./js/multitool/DragDropManager.js"; import ImageHighlighter from "./js/multitool/ImageHighlighter.js"; import PdfActionsManager from './js/multitool/PdfActionsManager.js'; - import FileDragManager from './js/multitool/fileInput.js'; // enables drag and drop + + const pdfUpload = document.querySelector("input[name=pdf-upload]"); + pdfUpload.addEventListener("change", async (e) => { + if (!e.target.files) return; + await pdfContainer.handleDroppedFiles( e.target.files); + e.target.dispatchEvent(new CustomEvent('reset', {})); + }); var undoManager = new UndoManager(); const dragDropManager = new DragDropManager('drag-container', 'pages-container'); @@ -189,7 +191,6 @@ const imageHighlighter = new ImageHighlighter('image-highlighter'); // enables the default action buttons on each file const pdfActionsManager = new PdfActionsManager('pages-container', undoManager); - const fileDragManager = new FileDragManager(); // Scroll the wrapper horizontally // Automatically exposes rotateAll, addFiles and exportPdf to the window for the global buttons. @@ -199,13 +200,11 @@ [ dragDropManager, imageHighlighter, - pdfActionsManager, - fileDragManager + pdfActionsManager ], undoManager ) - fileDragManager.setCallback(async (files) => pdfContainer.handleDroppedFiles(files)); document.addEventListener('keydown', function (event) { let targetElementId = event.target.id; diff --git a/src/main/resources/templates/pipeline.html b/src/main/resources/templates/pipeline.html index a81d4b91f..34abed18b 100644 --- a/src/main/resources/templates/pipeline.html +++ b/src/main/resources/templates/pipeline.html @@ -64,7 +64,7 @@
@@ -93,7 +93,7 @@