conflict fixed

This commit is contained in:
saikumarjetti 2025-03-25 19:57:53 +05:30
commit 0c0e148411
48 changed files with 1244 additions and 767 deletions

View File

@ -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 }}

View File

@ -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: |

View File

@ -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: |

View File

@ -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

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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

View File

@ -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: |

View File

@ -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 }}

View File

@ -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 }}

View File

@ -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 \

View File

@ -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 \

View File

@ -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) |

View File

@ -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

View File

@ -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;
}
}

View File

@ -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 =

View File

@ -48,6 +48,22 @@ public class EndpointConfiguration {
return endpointStatuses.getOrDefault(endpoint, true);
}
public boolean isGroupEnabled(String group) {
Set<String> 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

View File

@ -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<ContextRefreshedEvent> {
private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class);
private final ApplicationContext applicationContext;
private final Set<String> 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<String, RequestMappingHandlerMapping> mappings =
applicationContext.getBeansOfType(RequestMappingHandlerMapping.class);
for (Map.Entry<String, RequestMappingHandlerMapping> entry : mappings.entrySet()) {
RequestMappingHandlerMapping mapping = entry.getValue();
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerEntry :
handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = handlerEntry.getKey();
HandlerMethod handlerMethod = handlerEntry.getValue();
boolean isGetHandler = false;
try {
Set<RequestMethod> methods = mappingInfo.getMethodsCondition().getMethods();
isGetHandler = methods.isEmpty() || methods.contains(RequestMethod.GET);
} catch (Exception e) {
isGetHandler = true;
}
if (isGetHandler) {
Set<String> 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<String> extractPatternsUsingDirectPaths(RequestMappingInfo mappingInfo) {
Set<String> patterns = new HashSet<>();
try {
Method getDirectPathsMethod = mappingInfo.getClass().getMethod("getDirectPaths");
Object result = getDirectPathsMethod.invoke(mappingInfo);
if (result instanceof Set) {
@SuppressWarnings("unchecked")
Set<String> resultSet = (Set<String>) result;
patterns.addAll(resultSet);
}
} catch (Exception e) {
// Return empty set if method not found or fails
}
return patterns;
}
private Set<String> extractPatternsFromString(RequestMappingInfo mappingInfo) {
Set<String> 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<String> getValidGetEndpoints() {
if (!endpointsDiscovered) {
discoverEndpoints();
endpointsDiscovered = true;
}
return new HashSet<>(validGetEndpoints);
}
private void logAllEndpoints() {
Set<String> 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 ===");
}
}

View File

@ -333,7 +333,7 @@ public class UserController {
}
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
sessionRegistry.getAllSessions(username, false);
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());

View File

@ -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<String, List<ImageReference>> uniqueImages = findImages(doc);
// Collect all unique images by content hash
Map<String, List<ImageReference>> uniqueImages = new HashMap<>();
Map<String, PDImageXObject> 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<String, PDImageXObject> 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<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
String imageHash = entry.getKey();
List<ImageReference> 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<String, List<ImageReference>> findImages(PDDocument doc) throws IOException {
Map<String, List<ImageReference>> 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<String, List<ImageReference>> 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<String, List<ImageReference>> 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<String, List<ImageReference>> uniqueImages, CompressionStats stats) {
for (List<ImageReference> references : uniqueImages.values()) {
for (ImageReference ref : references) {
stats.totalImages++;
if (ref instanceof NestedImageReference) {
stats.nestedImages++;
}
}
}
}
// Create compressed versions of all unique images
private Map<String, PDImageXObject> createCompressedImages(
PDDocument doc,
Map<String, List<ImageReference>> uniqueImages,
double scaleFactor,
float jpegQuality,
boolean convertToGrayscale,
CompressionStats stats)
throws IOException {
Map<String, PDImageXObject> compressedVersions = new HashMap<>();
// Process each unique image exactly once
for (Entry<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
String imageHash = entry.getKey();
List<ImageReference> 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<String, List<ImageReference>> uniqueImages,
Map<String, PDImageXObject> compressedVersions,
CompressionStats stats)
throws IOException {
for (Entry<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
String imageHash = entry.getKey();
List<ImageReference> 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<Path> 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<String> 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<Path> 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<String> 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));

View File

@ -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<String> operationNames =
config.getOperations().stream()
.map(PipelineOperation::getOperation)
.collect(Collectors.toList());
Map<String, Object> properties = new HashMap<>();
properties.put("operations", operationNames);
properties.put("fileCount", files.length);
postHogService.captureEvent("pipeline_api_event", properties);
try {
List<Resource> inputFiles = processor.generateInputFiles(files);
if (inputFiles == null || inputFiles.size() == 0) {

View File

@ -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<String> operationNames =
config.getOperations().stream().map(PipelineOperation::getOperation).toList();
Map<String, Object> properties = new HashMap<>();
properties.put("operations", operationNames);
properties.put("fileCount", files.length);
postHogService.captureEvent("pipeline_directory_event", properties);
List<File> 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);
}
}
}

View File

@ -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<FontResource> getFontNamesFromLocation(String locationPattern) {
try {
Resource[] resources =
ResourcePatternUtils.getResourcePatternResolver(resourceLoader)
.getResources(locationPattern);
GeneralUtils.getResourcesFromLocationPattern(locationPattern, resourceLoader);
return Arrays.stream(resources)
.map(
resource -> {

View File

@ -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.")

View File

@ -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;
}

View File

@ -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<String, Double> 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<String, Object> 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);
}
}

View File

@ -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(

View File

@ -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

View File

@ -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 !

View File

@ -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,</br> 4-6 lite image compression,</br> 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,</br> 4-6: Compressão leve de Imagem,</br> 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 <a href="{0}">Multiferramentas de PDF</a>. 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!

View File

@ -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"
},

View File

@ -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 */

View File

@ -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);

View File

@ -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

View File

@ -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) {

View File

@ -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');

View File

@ -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');

View File

@ -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 = '<p>Drop files anywhere to upload</p>';
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;

View File

@ -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 () {

View File

@ -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');

View File

@ -196,7 +196,7 @@
</th:block>
<th:block th:fragment="fileSelector(name, multipleInputsForSingleRequest)"
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, notRequired=${notRequired} ?: false">
th:with="accept=${accept} ?: '*/*', inputText=${inputText} ?: #{pdfPrompt}, remoteCall=${remoteCall} ?: true, disableMultipleFiles=${disableMultipleFiles} ?: false, showUploads=${showUploads} ?: true, notRequired=${notRequired} ?: false">
<script th:inline="javascript">
(function () {
window.stirlingPDF.pdfPasswordPrompt = /*[[#{error.pdfPassword}]]*/ '';
@ -224,15 +224,20 @@
window.fileInput = {
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
extractPDF: '[[#{fileChooser.extractPDF}]]'
extractPDF: '[[#{fileChooser.extractPDF}]]',
loading: '[[#{loading}]]'
};</script>
<div class="custom-file-chooser mb-3"
th:attr="data-bs-unique-id=${name}, data-bs-element-id=${name+'-input'}, data-bs-element-container-id=${name+'-input-container'}, data-bs-files-selected=#{filesSelected}, data-bs-pdf-prompt=#{pdfPrompt}">
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}">
<div class="mb-3 d-flex flex-row justify-content-center align-items-center flex-wrap input-container"
th:name="${name}+'-input'" th:id="${name}+'-input-container'" th:data-text="#{fileChooser.hoveredDragAndDrop}">
<label class="file-input-btn d-none">
<input type="file" class="form-control" th:name="${name}" th:id="${name}+'-input'" th:accept="${accept} + ',.zip'"
th:attr="multiple=${!disableMultipleFiles}" th:required="${notRequired} ? null : 'required'">
<input type="file" class="form-control"
th:name="${name}"
th:id="${name}+'-input'"
th:accept="${accept == null ? '*/*': ((accept == '*/*') ? accept : (accept + ',.zip'))}"
th:attr="multiple=${!disableMultipleFiles}"
th:required="${notRequired} ? null : 'required'">
Browse
</label>
<div class="d-flex justify-content-start align-items-center" id="fileInputText">

View File

@ -24,7 +24,7 @@
border-bottom-width: 1px;
border-color: var(--md-nav-color-on-seperator)">
<div class="container ">
<a class="navbar-brand" th:href="@{'/'}" style="display: flex;">
<a class="navbar-brand" th:href="${@contextPath}" style="display: flex;">
<img class="main-icon" th:src="@{'/favicon.svg'}" alt="icon">
<span class="icon-text" th:text="${@navBarText}"></span>
</a>
@ -265,17 +265,18 @@
</div>
</div>
<script th:src="@{'/js/settings.js'}"></script>
<script>
<script th:inline="javascript">
window.onload = function () {
updateFavoritesDropdown();
}
document.addEventListener('DOMContentLoaded', function () {
const navbarLink = document.querySelector(".navbar-brand");
const contentPath = /*[[${@contextPath}]]*/ '';
if (localStorage.getItem("defaultView") === "home-legacy") {
navbarLink.setAttribute("href", "/home-legacy");
navbarLink.setAttribute("href", contentPath + "home-legacy");
} else {
navbarLink.setAttribute("href", "/");
navbarLink.setAttribute("href", contentPath);
}
});
</script>

View File

@ -219,7 +219,7 @@
window.analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
/*]]>*/
</script>
<script th:src="@{'/js/pages/home.js'}"></script>
<script th:src="@{'/js/pages/home.js'}" th:inline="javascript"></script>
</body>

View File

@ -52,8 +52,8 @@
const pagesTranslation = document.getElementById('pagesTranslation').innerText; // Get translation for multiple pages
</script>
<script type="module">
import * as pdfjsLib from '/pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs';
import * as pdfjsLib from './pdfjs-legacy/pdf.mjs';
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
</script>
<script th:src="@{'/js/merge.js'}"></script>
</div>

View File

@ -14,117 +14,113 @@
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon advance">construction</span>
<span class="tool-header-text" th:text="#{multiTool.header}"></span>
</div>
<div class="mt-action-bar d-flex flex-wrap">
<div class="mt-filename">
<label for="filename-input" th:text="#{multiTool.uploadPrompts}">Filename</label>
<input type="text" class="form-control" id="filename-input"
th:placeholder="#{multiTool.uploadPrompts}">
</div>
<div class="mt-action-btn">
<button class="btn btn-primary" th:title="#{multiTool.addFile}" onclick="addFiles()">
<span class="material-symbols-rounded">
add
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
onclick="rotateAll(-90)" disabled>
<span class="material-symbols-rounded">
rotate_left
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
onclick="rotateAll(90)" disabled>
<span class="material-symbols-rounded">
rotate_right
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
disabled>
<span class="material-symbols-rounded">
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
disabled>
<span class="material-symbols-rounded">
undo
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
disabled>
<span class="material-symbols-rounded">
redo
</span>
</button>
</button>
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
event_list
</span>
</button>
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
</button>
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="select-icon">select_all</span>
</button>
<div class="button-container">
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
class="btn btn-danger delete hidden" onclick="deleteSelected()">
<span class="material-symbols-rounded">delete</span>
</button>
</div>
<div style="margin-left:auto">
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
<span class="material-symbols-rounded">
file_save
</span>
</button>
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
onclick="exportPdf(false)" disabled>
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
Pages</h5>
<input type="text" id="csv-input" class="form-control" style="height:2.5rem" placeholder="1,3,5-10"
value="">
</div>
<ul id="selected-pages-list" class="pages-list"></ul>
<div class="mt-action-bar d-flex flex-wrap">
<div class="mt-filename">
<input type="text" class="form-control" id="filename-input"
th:placeholder="#{multiTool.uploadPrompts}">
</div>
<div class="mt-file-uploader">
<div
th:replace="~{fragments/common :: fileSelector(name='pdf-upload', multipleInputsForSingleRequest=false, accept='image/*, application/pdf', showUploads=false)}">
</div>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected
Pages</h5>
<input type="text" id="csv-input" class="form-control" style="height:2.5rem" placeholder="1,3,5-10"
value="">
</div>
<ul id="selected-pages-list" class="pages-list"></ul>
</div>
</div>
<div class="multi-tool-container">
<div class="d-flex flex-wrap" id="pages-container-wrapper">
<div id="pages-container">
</div>
</div>
</div>
<div class="mt-action-btn">
<button id="undo-btn" th:title="#{multiTool.undo}" class="btn btn-secondary" onclick="undo()"
disabled>
<span class="material-symbols-rounded">
undo
</span>
</button>
<button id="redo-btn" class="btn btn-secondary" th:title="#{multiTool.redo}" onclick="redo()"
disabled>
<span class="material-symbols-rounded">
redo
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateLeft}"
onclick="rotateAll(-90)" disabled>
<span class="material-symbols-rounded">
rotate_left
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.rotateRight}"
onclick="rotateAll(90)" disabled>
<span class="material-symbols-rounded">
rotate_right
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.split}" onclick="splitAll()"
disabled>
<span class="material-symbols-rounded">
cut
</span>
</button>
<button class="btn btn-secondary enable-on-file" th:title="#{multiTool.insertPageBreak}"
onclick="addFilesBlankAll()" disabled>
<span class="material-symbols-rounded">
insert_page_break
</span>
</button>
<button id="select-pages-container" th:title="#{multiTool.selectPages}"
class="btn btn-secondary enable-on-file" onclick="toggleSelectPageVisibility()" disabled>
<span id="select-pages-button" class="material-symbols-rounded">
event_list
</span>
</button>
<button id="deselect-All-Container" th:title="#{multiTool.deselectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="deselect-icon">deselect</span>
</button>
<button id="select-All-Container" th:title="#{multiTool.selectAll}"
class="btn btn-secondary enable-on-file hidden" onclick="toggleSelectAll()" disabled>
<span class="material-symbols-rounded" id="select-icon">select_all</span>
</button>
<button id="delete-button" th:title="#{multiTool.deleteSelected}"
class="btn btn-danger delete hidden" onclick="deleteSelected()">
<span class="material-symbols-rounded">delete</span>
</button>
<button id="export-selected-button" th:title="#{multiTool.downloadSelected}"
style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
class="btn btn-primary enable-on-file hidden" onclick="exportPdf(true)" disabled>
<span class="material-symbols-rounded">
file_save
</span>
</button>
<button style="border-color: green; color:#b2e3a8; background: rgba(24, 122, 5, 1)"
th:title="#{multiTool.downloadAll}" id="export-button" class="btn btn-primary enable-on-file"
onclick="exportPdf(false)" disabled>
<span class="material-symbols-rounded">
download
</span>
</button>
</div>
</div>
</div>
</div>
@ -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;

View File

@ -64,7 +64,7 @@
</div>
<div class="element-margin">
<div
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true)}"
th:replace="~{fragments/common :: fileSelector(name='fileInput', multipleInputsForSingleRequest=true, accept='*/*')}"
></div>
</div>
<div class="element-margin text-start">
@ -93,7 +93,7 @@
<!-- The Modal -->
<div class="modal" id="pipelineSettingsModal">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content dark-card">
<!-- Modal Header -->
<div class="modal-header">

View File

@ -212,6 +212,8 @@ main() {
cd "$PROJECT_ROOT"
export DOCKER_CLI_EXPERIMENTAL=enabled
export COMPOSE_DOCKER_CLI_BUILD=0
export DOCKER_ENABLE_SECURITY=false
# Run the gradlew build command and check if it fails
if ! ./gradlew clean build; then
@ -250,7 +252,7 @@ main() {
# Building Docker images with security enabled
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile .
# docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile.ultra-lite .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile.fat .
# Test each configuration with security