mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-06-23 16:05:09 +00:00
Compare commits
No commits in common. "8bfdb2abb5609e2fe30226e2915017cd9a949b00" and "523240554f29068c2b70a1738951e735ecb031a4" have entirely different histories.
8bfdb2abb5
...
523240554f
@ -180,7 +180,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push PR-specific image
|
- name: Build and push PR-specific image
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
2
.github/workflows/dependency-review.yml
vendored
2
.github/workflows/dependency-review.yml
vendored
@ -24,4 +24,4 @@ jobs:
|
|||||||
- name: "Checkout Repository"
|
- name: "Checkout Repository"
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: "Dependency Review"
|
- name: "Dependency Review"
|
||||||
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
|
uses: actions/dependency-review-action@38ecb5b593bf0eb19e335c03f97670f792489a8b # v4.7.0
|
||||||
|
2
.github/workflows/licenses-update.yml
vendored
2
.github/workflows/licenses-update.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "adopt"
|
distribution: "adopt"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
|
|
||||||
- name: check the licenses for compatibility
|
- name: check the licenses for compatibility
|
||||||
run: ./gradlew clean checkLicense
|
run: ./gradlew clean checkLicense
|
||||||
|
4
.github/workflows/multiOSReleases.yml
vendored
4
.github/workflows/multiOSReleases.yml
vendored
@ -68,7 +68,7 @@ jobs:
|
|||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@ -156,7 +156,7 @@ jobs:
|
|||||||
java-version: "21"
|
java-version: "21"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
|
8
.github/workflows/push-docker.yml
vendored
8
.github/workflows/push-docker.yml
vendored
@ -30,7 +30,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile
|
- name: Build and push main Dockerfile
|
||||||
id: build-push-regular
|
id: build-push-regular
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
context: .
|
context: .
|
||||||
@ -135,7 +135,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Dockerfile-ultra-lite
|
- name: Build and push Dockerfile-ultra-lite
|
||||||
id: build-push-lite
|
id: build-push-lite
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@ -166,7 +166,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push main Dockerfile fat
|
- name: Build and push main Dockerfile fat
|
||||||
id: build-push-fat
|
id: build-push-fat
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
if: github.ref != 'refs/heads/main'
|
if: github.ref != 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
2
.github/workflows/releaseArtifacts.yml
vendored
2
.github/workflows/releaseArtifacts.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
with:
|
with:
|
||||||
gradle-version: 8.14
|
gradle-version: 8.14
|
||||||
|
|
||||||
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
@ -74,6 +74,6 @@ jobs:
|
|||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
|
|
||||||
- name: Build and analyze with Gradle
|
- name: Build and analyze with Gradle
|
||||||
env:
|
env:
|
||||||
|
2
.github/workflows/swagger.yml
vendored
2
.github/workflows/swagger.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
|||||||
java-version: "17"
|
java-version: "17"
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
|
|
||||||
- uses: gradle/actions/setup-gradle@8379f6a1328ee0e06e2bb424dadb7b159856a326 # v4.4.0
|
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
|
||||||
|
|
||||||
- name: Generate Swagger documentation
|
- name: Generate Swagger documentation
|
||||||
run: ./gradlew generateOpenApiDocs
|
run: ./gradlew generateOpenApiDocs
|
||||||
|
2
.github/workflows/testdriver.yml
vendored
2
.github/workflows/testdriver.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
password: ${{ secrets.DOCKER_HUB_API }}
|
password: ${{ secrets.DOCKER_HUB_API }}
|
||||||
|
|
||||||
- name: Build and push test image
|
- name: Build and push test image
|
||||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
24
AGENTS.md
24
AGENTS.md
@ -1,24 +0,0 @@
|
|||||||
# Codex Contribution Guidelines for Stirling-PDF
|
|
||||||
|
|
||||||
This file provides high-level instructions for Codex when modifying any files within this repository. Follow these rules to ensure changes remain consistent with the existing project structure.
|
|
||||||
|
|
||||||
## 1. Code Style and Formatting
|
|
||||||
- Respect the `.editorconfig` settings located in the repository root. Java files use 4 spaces; HTML, JS, and Python generally use 2 spaces. Lines should end with `LF`.
|
|
||||||
- Format Java code with `./gradlew spotlessApply` before committing.
|
|
||||||
- Review `DeveloperGuide.md` for project structure and design details before making significant changes.
|
|
||||||
|
|
||||||
## 2. Testing
|
|
||||||
- Run `./gradlew build` before committing changes to ensure the project compiles.
|
|
||||||
- If the build cannot complete due to environment restrictions, DO NOT COMMIT THE CHANGE
|
|
||||||
|
|
||||||
## 3. Commits
|
|
||||||
- Keep commits focused. Group related changes together and provide concise commit messages.
|
|
||||||
- Ensure the working tree is clean (`git status`) before concluding your work.
|
|
||||||
|
|
||||||
## 4. Pull Requests
|
|
||||||
- Summarize what was changed and why. Include build results from `./gradlew build` in the PR description.
|
|
||||||
- Note that the code was generated with the assistance of AI.
|
|
||||||
|
|
||||||
## 5. Translations
|
|
||||||
- Only modify `messages_en_GB.properties` when adding or updating translations.
|
|
||||||
|
|
12
build.gradle
12
build.gradle
@ -10,7 +10,7 @@ plugins {
|
|||||||
id "com.github.jk1.dependency-license-report" version "2.9"
|
id "com.github.jk1.dependency-license-report" version "2.9"
|
||||||
//id "nebula.lint" version "19.0.3"
|
//id "nebula.lint" version "19.0.3"
|
||||||
id("org.panteleyev.jpackageplugin") version "1.6.1"
|
id("org.panteleyev.jpackageplugin") version "1.6.1"
|
||||||
id "org.sonarqube" version "6.2.0.5505"
|
id "org.sonarqube" version "6.1.0.5360"
|
||||||
}
|
}
|
||||||
|
|
||||||
import com.github.jk1.license.render.*
|
import com.github.jk1.license.render.*
|
||||||
@ -24,7 +24,7 @@ ext {
|
|||||||
imageioVersion = "3.12.0"
|
imageioVersion = "3.12.0"
|
||||||
lombokVersion = "1.18.38"
|
lombokVersion = "1.18.38"
|
||||||
bouncycastleVersion = "1.80"
|
bouncycastleVersion = "1.80"
|
||||||
springSecuritySamlVersion = "6.5.0"
|
springSecuritySamlVersion = "6.4.5"
|
||||||
openSamlVersion = "4.3.2"
|
openSamlVersion = "4.3.2"
|
||||||
tempJrePath = null
|
tempJrePath = null
|
||||||
}
|
}
|
||||||
@ -434,7 +434,7 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//security updates
|
//security updates
|
||||||
implementation "org.springframework:spring-webmvc:6.2.7"
|
implementation "org.springframework:spring-webmvc:6.2.6"
|
||||||
|
|
||||||
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
implementation("io.github.pixee:java-security-toolkit:1.2.1")
|
||||||
|
|
||||||
@ -459,7 +459,7 @@ dependencies {
|
|||||||
implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-mail:$springBootVersion"
|
||||||
|
|
||||||
implementation "org.springframework.session:spring-session-core:3.4.3"
|
implementation "org.springframework.session:spring-session-core:3.4.3"
|
||||||
implementation "org.springframework:spring-jdbc:6.2.7"
|
implementation "org.springframework:spring-jdbc:6.2.6"
|
||||||
|
|
||||||
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
|
||||||
// Don't upgrade h2database
|
// Don't upgrade h2database
|
||||||
@ -528,7 +528,7 @@ dependencies {
|
|||||||
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
|
||||||
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||||
implementation "io.micrometer:micrometer-core:1.15.0"
|
implementation "io.micrometer:micrometer-core:1.14.7"
|
||||||
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
|
||||||
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
// https://mvnrepository.com/artifact/org.commonmark/commonmark
|
||||||
implementation "org.commonmark:commonmark:0.24.0"
|
implementation "org.commonmark:commonmark:0.24.0"
|
||||||
@ -544,7 +544,7 @@ dependencies {
|
|||||||
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
annotationProcessor "org.projectlombok:lombok:$lombokVersion"
|
||||||
|
|
||||||
// Mockito (core)
|
// Mockito (core)
|
||||||
testImplementation 'org.mockito:mockito-core:5.17.0'
|
testImplementation 'org.mockito:mockito-core:5.11.0'
|
||||||
|
|
||||||
|
|
||||||
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
plugins {
|
plugins {
|
||||||
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
// Apply the foojay-resolver plugin to allow automatic download of JDKs
|
||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
|
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
|
||||||
}
|
}
|
||||||
rootProject.name = 'Stirling-PDF'
|
rootProject.name = 'Stirling-PDF'
|
||||||
|
@ -49,8 +49,7 @@ public class KeygenLicenseVerifier {
|
|||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
|
|
||||||
// Shared HTTP client for connection pooling
|
// Shared HTTP client for connection pooling
|
||||||
private static final HttpClient httpClient =
|
private static final HttpClient httpClient = HttpClient.newBuilder()
|
||||||
HttpClient.newBuilder()
|
|
||||||
.version(HttpClient.Version.HTTP_2)
|
.version(HttpClient.Version.HTTP_2)
|
||||||
.connectTimeout(java.time.Duration.ofSeconds(10))
|
.connectTimeout(java.time.Duration.ofSeconds(10))
|
||||||
.build();
|
.build();
|
||||||
@ -417,9 +416,7 @@ public class KeygenLicenseVerifier {
|
|||||||
if (policyFloating) {
|
if (policyFloating) {
|
||||||
context.isFloatingLicense = true;
|
context.isFloatingLicense = true;
|
||||||
context.maxMachines = policyMaxMachines;
|
context.maxMachines = policyMaxMachines;
|
||||||
log.info(
|
log.info("Policy defines floating license with max machines: {}", context.maxMachines);
|
||||||
"Policy defines floating license with max machines: {}",
|
|
||||||
context.maxMachines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract max users and isEnterprise from policy or metadata
|
// Extract max users and isEnterprise from policy or metadata
|
||||||
@ -477,8 +474,7 @@ public class KeygenLicenseVerifier {
|
|||||||
activateMachine(licenseKey, licenseId, machineFingerprint, context);
|
activateMachine(licenseKey, licenseId, machineFingerprint, context);
|
||||||
if (activated) {
|
if (activated) {
|
||||||
// Revalidate after activation
|
// Revalidate after activation
|
||||||
validationResponse =
|
validationResponse = validateLicense(licenseKey, machineFingerprint, context);
|
||||||
validateLicense(licenseKey, machineFingerprint, context);
|
|
||||||
isValid =
|
isValid =
|
||||||
validationResponse != null
|
validationResponse != null
|
||||||
&& validationResponse
|
&& validationResponse
|
||||||
@ -498,8 +494,8 @@ public class KeygenLicenseVerifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonNode validateLicense(
|
private JsonNode validateLicense(String licenseKey, String machineFingerprint, LicenseContext context)
|
||||||
String licenseKey, String machineFingerprint, LicenseContext context) throws Exception {
|
throws Exception {
|
||||||
String requestBody =
|
String requestBody =
|
||||||
String.format(
|
String.format(
|
||||||
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
"{\"meta\":{\"key\":\"%s\",\"scope\":{\"fingerprint\":\"%s\"}}}",
|
||||||
@ -518,8 +514,7 @@ public class KeygenLicenseVerifier {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response =
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
log.info("ValidateLicenseResponse body: {}", response.body());
|
log.info("ValidateLicenseResponse body: {}", response.body());
|
||||||
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
JsonNode jsonResponse = objectMapper.readTree(response.body());
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
@ -539,10 +534,8 @@ public class KeygenLicenseVerifier {
|
|||||||
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
|
context.isFloatingLicense = licenseAttrs.path("floating").asBoolean(false);
|
||||||
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
|
context.maxMachines = licenseAttrs.path("maxMachines").asInt(1);
|
||||||
|
|
||||||
log.info(
|
log.info("License floating (from license): {}, maxMachines: {}",
|
||||||
"License floating (from license): {}, maxMachines: {}",
|
context.isFloatingLicense, context.maxMachines);
|
||||||
context.isFloatingLicense,
|
|
||||||
context.maxMachines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check the policy for floating license support if included
|
// Also check the policy for floating license support if included
|
||||||
@ -560,8 +553,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
if (policyNode != null) {
|
if (policyNode != null) {
|
||||||
// Check if this is a floating license from policy
|
// Check if this is a floating license from policy
|
||||||
boolean policyFloating =
|
boolean policyFloating = policyNode.path("attributes").path("floating").asBoolean(false);
|
||||||
policyNode.path("attributes").path("floating").asBoolean(false);
|
|
||||||
int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1);
|
int policyMaxMachines = policyNode.path("attributes").path("maxMachines").asInt(1);
|
||||||
|
|
||||||
// Policy takes precedence over license attributes
|
// Policy takes precedence over license attributes
|
||||||
@ -570,10 +562,8 @@ public class KeygenLicenseVerifier {
|
|||||||
context.maxMachines = policyMaxMachines;
|
context.maxMachines = policyMaxMachines;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info(
|
log.info("License floating (from policy): {}, maxMachines: {}",
|
||||||
"License floating (from policy): {}, maxMachines: {}",
|
context.isFloatingLicense, context.maxMachines);
|
||||||
context.isFloatingLicense,
|
|
||||||
context.maxMachines);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract user count, default to 1 if not specified
|
// Extract user count, default to 1 if not specified
|
||||||
@ -603,14 +593,11 @@ public class KeygenLicenseVerifier {
|
|||||||
return jsonResponse;
|
return jsonResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean activateMachine(
|
private boolean activateMachine(String licenseKey, String licenseId, String machineFingerprint,
|
||||||
String licenseKey, String licenseId, String machineFingerprint, LicenseContext context)
|
LicenseContext context) throws Exception {
|
||||||
throws Exception {
|
|
||||||
// For floating licenses, we first need to check if we need to deregister any machines
|
// For floating licenses, we first need to check if we need to deregister any machines
|
||||||
if (context.isFloatingLicense) {
|
if (context.isFloatingLicense) {
|
||||||
log.info(
|
log.info("Processing floating license activation. Max machines allowed: {}", context.maxMachines);
|
||||||
"Processing floating license activation. Max machines allowed: {}",
|
|
||||||
context.maxMachines);
|
|
||||||
|
|
||||||
// Get the current machines for this license
|
// Get the current machines for this license
|
||||||
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
|
JsonNode machinesResponse = fetchMachinesForLicense(licenseKey, licenseId);
|
||||||
@ -618,23 +605,17 @@ public class KeygenLicenseVerifier {
|
|||||||
JsonNode machines = machinesResponse.path("data");
|
JsonNode machines = machinesResponse.path("data");
|
||||||
int currentMachines = machines.size();
|
int currentMachines = machines.size();
|
||||||
|
|
||||||
log.info(
|
log.info("Current machine count: {}, Max allowed: {}", currentMachines, context.maxMachines);
|
||||||
"Current machine count: {}, Max allowed: {}",
|
|
||||||
currentMachines,
|
|
||||||
context.maxMachines);
|
|
||||||
|
|
||||||
// Check if the current fingerprint is already activated
|
// Check if the current fingerprint is already activated
|
||||||
boolean isCurrentMachineActivated = false;
|
boolean isCurrentMachineActivated = false;
|
||||||
String currentMachineId = null;
|
String currentMachineId = null;
|
||||||
|
|
||||||
for (JsonNode machine : machines) {
|
for (JsonNode machine : machines) {
|
||||||
if (machineFingerprint.equals(
|
if (machineFingerprint.equals(machine.path("attributes").path("fingerprint").asText())) {
|
||||||
machine.path("attributes").path("fingerprint").asText())) {
|
|
||||||
isCurrentMachineActivated = true;
|
isCurrentMachineActivated = true;
|
||||||
currentMachineId = machine.path("id").asText();
|
currentMachineId = machine.path("id").asText();
|
||||||
log.info(
|
log.info("Current machine is already activated with ID: {}", currentMachineId);
|
||||||
"Current machine is already activated with ID: {}",
|
|
||||||
currentMachineId);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -647,8 +628,7 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
// If we've reached the max machines limit, we need to deregister the oldest machine
|
// If we've reached the max machines limit, we need to deregister the oldest machine
|
||||||
if (currentMachines >= context.maxMachines) {
|
if (currentMachines >= context.maxMachines) {
|
||||||
log.info(
|
log.info("Max machines reached. Deregistering oldest machine to make room for the new machine.");
|
||||||
"Max machines reached. Deregistering oldest machine to make room for the new machine.");
|
|
||||||
|
|
||||||
// Find the oldest machine based on creation timestamp
|
// Find the oldest machine based on creation timestamp
|
||||||
if (machines.size() > 0) {
|
if (machines.size() > 0) {
|
||||||
@ -657,28 +637,23 @@ public class KeygenLicenseVerifier {
|
|||||||
java.time.Instant oldestTime = null;
|
java.time.Instant oldestTime = null;
|
||||||
|
|
||||||
for (JsonNode machine : machines) {
|
for (JsonNode machine : machines) {
|
||||||
String createdStr =
|
String createdStr = machine.path("attributes").path("created").asText(null);
|
||||||
machine.path("attributes").path("created").asText(null);
|
|
||||||
if (createdStr != null && !createdStr.isEmpty()) {
|
if (createdStr != null && !createdStr.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
java.time.Instant createdTime =
|
java.time.Instant createdTime = java.time.Instant.parse(createdStr);
|
||||||
java.time.Instant.parse(createdStr);
|
|
||||||
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
if (oldestTime == null || createdTime.isBefore(oldestTime)) {
|
||||||
oldestTime = createdTime;
|
oldestTime = createdTime;
|
||||||
oldestMachineId = machine.path("id").asText();
|
oldestMachineId = machine.path("id").asText();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn(
|
log.warn("Could not parse creation time for machine: {}", e.getMessage());
|
||||||
"Could not parse creation time for machine: {}",
|
|
||||||
e.getMessage());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't determine the oldest by timestamp, use the first one
|
// If we couldn't determine the oldest by timestamp, use the first one
|
||||||
if (oldestMachineId == null) {
|
if (oldestMachineId == null) {
|
||||||
log.warn(
|
log.warn("Could not determine oldest machine by timestamp, using first machine in list");
|
||||||
"Could not determine oldest machine by timestamp, using first machine in list");
|
|
||||||
oldestMachineId = machines.path(0).path("id").asText();
|
oldestMachineId = machines.path(0).path("id").asText();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -686,15 +661,12 @@ public class KeygenLicenseVerifier {
|
|||||||
|
|
||||||
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
|
boolean deregistered = deregisterMachine(licenseKey, oldestMachineId);
|
||||||
if (!deregistered) {
|
if (!deregistered) {
|
||||||
log.error(
|
log.error("Failed to deregister machine. Cannot proceed with activation.");
|
||||||
"Failed to deregister machine. Cannot proceed with activation.");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
log.info(
|
log.info("Machine deregistered successfully. Proceeding with activation of new machine.");
|
||||||
"Machine deregistered successfully. Proceeding with activation of new machine.");
|
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error("License has reached machine limit but no machines were found to deregister. This is unexpected.");
|
||||||
"License has reached machine limit but no machines were found to deregister. This is unexpected.");
|
|
||||||
// We'll still try to activate, but it might fail
|
// We'll still try to activate, but it might fail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -748,8 +720,7 @@ public class KeygenLicenseVerifier {
|
|||||||
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response =
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
log.info("activateMachine Response body: " + response.body());
|
log.info("activateMachine Response body: " + response.body());
|
||||||
if (response.statusCode() == 201) {
|
if (response.statusCode() == 201) {
|
||||||
log.info("Machine activated successfully");
|
log.info("Machine activated successfully");
|
||||||
@ -777,33 +748,22 @@ public class KeygenLicenseVerifier {
|
|||||||
* @throws Exception if an error occurs during the HTTP request
|
* @throws Exception if an error occurs during the HTTP request
|
||||||
*/
|
*/
|
||||||
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
|
private JsonNode fetchMachinesForLicense(String licenseKey, String licenseId) throws Exception {
|
||||||
HttpRequest request =
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
HttpRequest.newBuilder()
|
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/licenses/" + licenseId + "/machines"))
|
||||||
.uri(
|
|
||||||
URI.create(
|
|
||||||
BASE_URL
|
|
||||||
+ "/"
|
|
||||||
+ ACCOUNT_ID
|
|
||||||
+ "/licenses/"
|
|
||||||
+ licenseId
|
|
||||||
+ "/machines"))
|
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
.header("Accept", "application/vnd.api+json")
|
.header("Accept", "application/vnd.api+json")
|
||||||
.header("Authorization", "License " + licenseKey)
|
.header("Authorization", "License " + licenseKey)
|
||||||
.GET()
|
.GET()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response =
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
log.info("fetchMachinesForLicense Response body: {}", response.body());
|
log.info("fetchMachinesForLicense Response body: {}", response.body());
|
||||||
|
|
||||||
if (response.statusCode() == 200) {
|
if (response.statusCode() == 200) {
|
||||||
return objectMapper.readTree(response.body());
|
return objectMapper.readTree(response.body());
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error("Error fetching machines for license. Status code: {}, error: {}",
|
||||||
"Error fetching machines for license. Status code: {}, error: {}",
|
response.statusCode(), response.body());
|
||||||
response.statusCode(),
|
|
||||||
response.body());
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -817,8 +777,7 @@ public class KeygenLicenseVerifier {
|
|||||||
*/
|
*/
|
||||||
private boolean deregisterMachine(String licenseKey, String machineId) {
|
private boolean deregisterMachine(String licenseKey, String machineId) {
|
||||||
try {
|
try {
|
||||||
HttpRequest request =
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
|
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines/" + machineId))
|
||||||
.header("Content-Type", "application/vnd.api+json")
|
.header("Content-Type", "application/vnd.api+json")
|
||||||
.header("Accept", "application/vnd.api+json")
|
.header("Accept", "application/vnd.api+json")
|
||||||
@ -826,17 +785,14 @@ public class KeygenLicenseVerifier {
|
|||||||
.DELETE()
|
.DELETE()
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
HttpResponse<String> response =
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
|
||||||
|
|
||||||
if (response.statusCode() == 204) {
|
if (response.statusCode() == 204) {
|
||||||
log.info("Machine {} successfully deregistered", machineId);
|
log.info("Machine {} successfully deregistered", machineId);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
log.error(
|
log.error("Error deregistering machine. Status code: {}, error: {}",
|
||||||
"Error deregistering machine. Status code: {}, error: {}",
|
response.statusCode(), response.body());
|
||||||
response.statusCode(),
|
|
||||||
response.body());
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -31,8 +31,7 @@ public class LibreOfficeListener {
|
|||||||
log.info("waiting for listener to start");
|
log.info("waiting for listener to start");
|
||||||
try (Socket socket = new Socket()) {
|
try (Socket socket = new Socket()) {
|
||||||
socket.connect(
|
socket.connect(
|
||||||
new InetSocketAddress("localhost", LISTENER_PORT),
|
new InetSocketAddress("localhost", 2002), 1000); // Timeout after 1 second
|
||||||
1000); // Timeout after 1 second
|
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -11,11 +11,8 @@ import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
|
|||||||
import org.thymeleaf.templateresource.FileTemplateResource;
|
import org.thymeleaf.templateresource.FileTemplateResource;
|
||||||
import org.thymeleaf.templateresource.ITemplateResource;
|
import org.thymeleaf.templateresource.ITemplateResource;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.InputStreamTemplateResource;
|
import stirling.software.SPDF.model.InputStreamTemplateResource;
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
|
||||||
|
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
@ -43,8 +40,7 @@ public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateRe
|
|||||||
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// Log the exception to help with debugging issues loading external templates
|
|
||||||
log.warn("Unable to read template '{}' from file system", resourceName, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InputStream inputStream =
|
InputStream inputStream =
|
||||||
|
@ -93,7 +93,6 @@ public class PipelineProcessor {
|
|||||||
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream logStream = new ByteArrayOutputStream();
|
||||||
PrintStream logPrintStream = new PrintStream(logStream);
|
PrintStream logPrintStream = new PrintStream(logStream);
|
||||||
boolean hasErrors = false;
|
boolean hasErrors = false;
|
||||||
boolean filtersApplied = false;
|
|
||||||
for (PipelineOperation pipelineOperation : config.getOperations()) {
|
for (PipelineOperation pipelineOperation : config.getOperations()) {
|
||||||
String operation = pipelineOperation.getOperation();
|
String operation = pipelineOperation.getOperation();
|
||||||
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
boolean isMultiInputOperation = apiDocService.isMultiInput(operation);
|
||||||
@ -135,7 +134,7 @@ public class PipelineProcessor {
|
|||||||
if (operation.startsWith("filter-")
|
if (operation.startsWith("filter-")
|
||||||
&& (response.getBody() == null
|
&& (response.getBody() == null
|
||||||
|| response.getBody().length == 0)) {
|
|| response.getBody().length == 0)) {
|
||||||
filtersApplied = true;
|
result.setFiltersApplied(true);
|
||||||
log.info("Skipping file due to filtering {}", operation);
|
log.info("Skipping file due to filtering {}", operation);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -216,12 +215,12 @@ public class PipelineProcessor {
|
|||||||
log.error("Errors occurred during processing. Log: {}", logStream.toString());
|
log.error("Errors occurred during processing. Log: {}", logStream.toString());
|
||||||
}
|
}
|
||||||
result.setHasErrors(hasErrors);
|
result.setHasErrors(hasErrors);
|
||||||
result.setFiltersApplied(filtersApplied);
|
result.setFiltersApplied(hasErrors);
|
||||||
result.setOutputFiles(outputFiles);
|
result.setOutputFiles(outputFiles);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
|
private ResponseEntity<byte[]> sendWebRequest(String url, MultiValueMap<String, Object> body) {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
// Set up headers, including API key
|
// Set up headers, including API key
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
@ -77,8 +77,9 @@ public class HomeWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/home-legacy")
|
@GetMapping("/home-legacy")
|
||||||
public String redirectHomeLegacy() {
|
public String homeLegacy(Model model) {
|
||||||
return "redirect:/";
|
model.addAttribute("currentPage", "home-legacy");
|
||||||
|
return "home-legacy";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
@GetMapping(value = "/robots.txt", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
@ -39,6 +39,7 @@ public class InputStreamTemplateResource implements ITemplateResource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists() {
|
public boolean exists() {
|
||||||
return inputStream != null;
|
// TODO Auto-generated method stub
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -553,7 +553,7 @@
|
|||||||
{
|
{
|
||||||
"moduleName": "io.micrometer:micrometer-core",
|
"moduleName": "io.micrometer:micrometer-core",
|
||||||
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
"moduleUrl": "https://github.com/micrometer-metrics/micrometer",
|
||||||
"moduleVersion": "1.15.0",
|
"moduleVersion": "1.14.7",
|
||||||
"moduleLicense": "The Apache Software License, Version 2.0",
|
"moduleLicense": "The Apache Software License, Version 2.0",
|
||||||
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
"moduleLicenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.txt"
|
||||||
},
|
},
|
||||||
@ -1637,7 +1637,7 @@
|
|||||||
{
|
{
|
||||||
"moduleName": "org.springframework.security:spring-security-saml2-service-provider",
|
"moduleName": "org.springframework.security:spring-security-saml2-service-provider",
|
||||||
"moduleUrl": "https://spring.io/projects/spring-security",
|
"moduleUrl": "https://spring.io/projects/spring-security",
|
||||||
"moduleVersion": "6.5.0",
|
"moduleVersion": "6.4.5",
|
||||||
"moduleLicense": "Apache License, Version 2.0",
|
"moduleLicense": "Apache License, Version 2.0",
|
||||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||||
},
|
},
|
||||||
@ -1714,7 +1714,7 @@
|
|||||||
{
|
{
|
||||||
"moduleName": "org.springframework:spring-jdbc",
|
"moduleName": "org.springframework:spring-jdbc",
|
||||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||||
"moduleVersion": "6.2.7",
|
"moduleVersion": "6.2.6",
|
||||||
"moduleLicense": "Apache License, Version 2.0",
|
"moduleLicense": "Apache License, Version 2.0",
|
||||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||||
},
|
},
|
||||||
@ -1742,7 +1742,7 @@
|
|||||||
{
|
{
|
||||||
"moduleName": "org.springframework:spring-webmvc",
|
"moduleName": "org.springframework:spring-webmvc",
|
||||||
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
"moduleUrl": "https://github.com/spring-projects/spring-framework",
|
||||||
"moduleVersion": "6.2.7",
|
"moduleVersion": "6.2.6",
|
||||||
"moduleLicense": "Apache License, Version 2.0",
|
"moduleLicense": "Apache License, Version 2.0",
|
||||||
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
"moduleLicenseUrl": "https://www.apache.org/licenses/LICENSE-2.0"
|
||||||
},
|
},
|
||||||
|
229
src/main/resources/static/css/home-legacy.css
Normal file
229
src/main/resources/static/css/home-legacy.css
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
#searchBar {
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
background-color: var(--md-sys-color-surface-container-low);
|
||||||
|
width: 100%;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 0.75rem 3.5rem;
|
||||||
|
border: 1px solid var(--md-sys-color-outline-variant);
|
||||||
|
border-radius: 3rem;
|
||||||
|
outline-color: var(--md-sys-color-outline-variant);
|
||||||
|
}
|
||||||
|
|
||||||
|
#filtersContainer {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
color: var(--md-sys-color-secondary);
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
transform-origin: center center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
margin: 0.75rem 1rem;
|
||||||
|
border: 0.1rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-group-legacy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-group-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(15rem, 3fr));
|
||||||
|
gap: 30px 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: -20px;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing:content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-group-container.animated-group {
|
||||||
|
transition: 0.5s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-group-legacy.collapsed>.feature-group-container {
|
||||||
|
max-height: 0 !important;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-expand-button {
|
||||||
|
transition: 0.5s all;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-expand-button.collapsed {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
border: 1px solid var(--md-sys-color-surface-5);
|
||||||
|
border-radius: 1.75rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
background: var(--md-sys-color-surface-5);
|
||||||
|
transition:
|
||||||
|
transform 0.3s,
|
||||||
|
border 0.3s;
|
||||||
|
transform-origin: center center;
|
||||||
|
outline: 0px solid transparent;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--md-sys-color-on-surface);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card .card-text {
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.08);
|
||||||
|
box-shadow: var(--md-sys-elevation-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title.text-primary {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-card-icon {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-icon {
|
||||||
|
display: none !important;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
color: var(--md-sys-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tool-icon {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tool-text {
|
||||||
|
margin: 0.0rem 0 0 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only show the favorite icons when the parent card is being hovered over */
|
||||||
|
.feature-card:hover .favorite-icon {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-icon img {
|
||||||
|
filter: brightness(0) invert(var(--md-theme-filter-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-icon:hover .material-symbols-rounded {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-icon .material-symbols-rounded.fill{
|
||||||
|
color: #f5c000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jumbotron {
|
||||||
|
padding: 3rem 3rem;
|
||||||
|
/* Reduce vertical padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.lookatme {
|
||||||
|
opacity: 1;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lookatme::after {
|
||||||
|
color: #e33100;
|
||||||
|
text-shadow: 0 0 5px #e33100;
|
||||||
|
/* in the html, the data-lookatme-text attribute must */
|
||||||
|
/* contain the same text as the .lookatme element */
|
||||||
|
content: attr(data-lookatme-text);
|
||||||
|
padding: inherit;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 0 0;
|
||||||
|
z-index: 1;
|
||||||
|
/* 20 steps / 2 seconds = 10fps */
|
||||||
|
-webkit-animation: 2s infinite Pulse steps(20);
|
||||||
|
animation: 2s infinite Pulse steps(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes Pulse {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.update-notice {
|
||||||
|
animation: scale 1s infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
@ -126,7 +126,11 @@ function addToFavorites(entryId) {
|
|||||||
localStorage.setItem('favoritesList', JSON.stringify(favoritesList));
|
localStorage.setItem('favoritesList', JSON.stringify(favoritesList));
|
||||||
updateFavoritesDropdown();
|
updateFavoritesDropdown();
|
||||||
updateFavoriteIcons();
|
updateFavoriteIcons();
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
if (currentPath.includes('home-legacy')) {
|
||||||
|
syncFavoritesLegacy();
|
||||||
|
} else {
|
||||||
initializeCards();
|
initializeCards();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
266
src/main/resources/static/js/homecard-legacy.js
Normal file
266
src/main/resources/static/js/homecard-legacy.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
function filterCardsLegacy() {
|
||||||
|
var input = document.getElementById('searchBar');
|
||||||
|
var filter = input.value.toUpperCase();
|
||||||
|
|
||||||
|
let featureGroups = document.querySelectorAll('.feature-group-legacy');
|
||||||
|
const collapsedGroups = getCollapsedGroups();
|
||||||
|
|
||||||
|
for (const featureGroup of featureGroups) {
|
||||||
|
var cards = featureGroup.querySelectorAll('.feature-card');
|
||||||
|
|
||||||
|
let groupMatchesFilter = false;
|
||||||
|
for (var i = 0; i < cards.length; i++) {
|
||||||
|
var card = cards[i];
|
||||||
|
var title = card.querySelector('h5.card-title').innerText;
|
||||||
|
var text = card.querySelector('p.card-text').innerText;
|
||||||
|
|
||||||
|
// Get the navbar tags associated with the card
|
||||||
|
var navbarItem = document.querySelector(`a.dropdown-item[href="${card.id}"]`);
|
||||||
|
var navbarTags = navbarItem ? navbarItem.getAttribute('data-bs-tags') : '';
|
||||||
|
|
||||||
|
var content = title + ' ' + text + ' ' + navbarTags;
|
||||||
|
|
||||||
|
if (content.toUpperCase().indexOf(filter) > -1) {
|
||||||
|
card.style.display = '';
|
||||||
|
groupMatchesFilter = true;
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupMatchesFilter) {
|
||||||
|
featureGroup.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
featureGroup.style.display = '';
|
||||||
|
resetOrTemporarilyExpandGroup(featureGroup, filter, collapsedGroups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCollapsedGroups() {
|
||||||
|
return localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOrTemporarilyExpandGroup(featureGroup, filterKeywords = '', collapsedGroups = []) {
|
||||||
|
const shouldResetCollapse = filterKeywords.trim() === '';
|
||||||
|
if (shouldResetCollapse) {
|
||||||
|
// Resetting the group's expand/collapse to its original state (as in collapsed groups)
|
||||||
|
const isCollapsed = collapsedGroups.indexOf(featureGroup.id) != -1;
|
||||||
|
expandCollapseToggle(featureGroup, !isCollapsed);
|
||||||
|
} else {
|
||||||
|
// Temporarily expands feature group without affecting the actual/stored collapsed groups
|
||||||
|
featureGroup.classList.remove('collapsed');
|
||||||
|
featureGroup.querySelector('.header-expand-button').classList.remove('collapsed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFavoritesSectionLegacy() {
|
||||||
|
const favoritesContainer = document.getElementById('groupFavorites').querySelector('.feature-group-container');
|
||||||
|
favoritesContainer.innerHTML = '';
|
||||||
|
const cards = Array.from(document.querySelectorAll('.feature-card:not(.duplicate)'));
|
||||||
|
const addedCardIds = new Set();
|
||||||
|
let favoritesAmount = 0;
|
||||||
|
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const favouritesList = JSON.parse(localStorage.getItem('favoritesList') || '[]');
|
||||||
|
|
||||||
|
if (favouritesList.includes(card.id) && !addedCardIds.has(card.id)) {
|
||||||
|
const duplicate = card.cloneNode(true);
|
||||||
|
duplicate.classList.add('duplicate');
|
||||||
|
favoritesContainer.appendChild(duplicate);
|
||||||
|
addedCardIds.add(card.id);
|
||||||
|
favoritesAmount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (favoritesAmount === 0) {
|
||||||
|
document.getElementById('groupFavorites').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
document.getElementById('groupFavorites').style.display = 'flex';
|
||||||
|
}
|
||||||
|
reorderCards(favoritesContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncFavoritesLegacy() {
|
||||||
|
const cards = Array.from(document.querySelectorAll('.feature-card'));
|
||||||
|
cards.forEach((card) => {
|
||||||
|
const isFavorite = localStorage.getItem(card.id) === 'favorite';
|
||||||
|
const starIcon = card.querySelector('.favorite-icon span.material-symbols-rounded');
|
||||||
|
if (starIcon) {
|
||||||
|
if (isFavorite) {
|
||||||
|
starIcon.classList.remove('no-fill');
|
||||||
|
starIcon.classList.add('fill');
|
||||||
|
card.classList.add('favorite');
|
||||||
|
} else {
|
||||||
|
starIcon.classList.remove('fill');
|
||||||
|
starIcon.classList.add('no-fill');
|
||||||
|
card.classList.remove('favorite');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
updateFavoritesSectionLegacy();
|
||||||
|
updateFavoritesDropdown();
|
||||||
|
filterCardsLegacy();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorderCards(container) {
|
||||||
|
var cards = Array.from(container.querySelectorAll('.feature-card'));
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
container.removeChild(card);
|
||||||
|
});
|
||||||
|
cards.sort(function (a, b) {
|
||||||
|
var aIsFavorite = localStorage.getItem(a.id) === 'favorite';
|
||||||
|
var bIsFavorite = localStorage.getItem(b.id) === 'favorite';
|
||||||
|
if (a.id === 'update-link') {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (b.id === 'update-link') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aIsFavorite && !bIsFavorite) {
|
||||||
|
return -1;
|
||||||
|
} else if (!aIsFavorite && bIsFavorite) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return a.id > b.id;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cards.forEach(function (card) {
|
||||||
|
container.appendChild(card);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reorderAllCards() {
|
||||||
|
const containers = Array.from(document.querySelectorAll('.feature-group-container'));
|
||||||
|
containers.forEach(function (container) {
|
||||||
|
reorderCards(container);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCardsLegacy() {
|
||||||
|
reorderAllCards();
|
||||||
|
updateFavoritesSectionLegacy();
|
||||||
|
updateFavoritesDropdown();
|
||||||
|
filterCardsLegacy();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFavoritesOnly() {
|
||||||
|
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||||
|
if (localStorage.getItem('favoritesOnly') === 'true') {
|
||||||
|
groups.forEach((group) => {
|
||||||
|
if (group.id !== 'groupFavorites') {
|
||||||
|
group.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
groups.forEach((group) => {
|
||||||
|
if (group.id !== 'groupFavorites') {
|
||||||
|
group.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFavoritesOnly() {
|
||||||
|
if (localStorage.getItem('favoritesOnly') === 'true') {
|
||||||
|
localStorage.setItem('favoritesOnly', 'false');
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('favoritesOnly', 'true');
|
||||||
|
}
|
||||||
|
showFavoritesOnly();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expands a feature group on true, collapses it on false and toggles state on null.
|
||||||
|
function expandCollapseToggle(group, expand = null) {
|
||||||
|
if (expand === null) {
|
||||||
|
group.classList.toggle('collapsed');
|
||||||
|
group.querySelector('.header-expand-button').classList.toggle('collapsed');
|
||||||
|
} else if (expand) {
|
||||||
|
group.classList.remove('collapsed');
|
||||||
|
group.querySelector('.header-expand-button').classList.remove('collapsed');
|
||||||
|
} else {
|
||||||
|
group.classList.add('collapsed');
|
||||||
|
group.querySelector('.header-expand-button').classList.add('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||||
|
const groupIndex = collapsed.indexOf(group.id);
|
||||||
|
|
||||||
|
if (group.classList.contains('collapsed')) {
|
||||||
|
if (groupIndex === -1) {
|
||||||
|
collapsed.push(group.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (groupIndex !== -1) {
|
||||||
|
collapsed.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('collapsedGroups', JSON.stringify(collapsed));
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandCollapseAll(expandAll) {
|
||||||
|
const groups = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||||
|
groups.forEach((group) => {
|
||||||
|
expandCollapseToggle(group, expandAll);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onload = function () {
|
||||||
|
initializeCardsLegacy();
|
||||||
|
syncFavoritesLegacy(); // Ensure everything is in sync on page load
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const materialIcons = new FontFaceObserver('Material Symbols Rounded');
|
||||||
|
|
||||||
|
materialIcons
|
||||||
|
.load()
|
||||||
|
.then(() => {
|
||||||
|
document.querySelectorAll('.feature-card.hidden').forEach((el) => {
|
||||||
|
el.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error('Material Symbols Rounded font failed to load.');
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll('.feature-group-header-legacy')).forEach((header) => {
|
||||||
|
const parent = header.parentNode;
|
||||||
|
const container = header.parentNode.querySelector('.feature-group-container');
|
||||||
|
if (parent.id !== 'groupFavorites') {
|
||||||
|
// container.style.maxHeight = container.scrollHeight + 'px';
|
||||||
|
}
|
||||||
|
header.onclick = () => {
|
||||||
|
expandCollapseToggle(parent);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const collapsed = localStorage.getItem('collapsedGroups') ? JSON.parse(localStorage.getItem('collapsedGroups')) : [];
|
||||||
|
const groupsArray = Array.from(document.querySelectorAll('.feature-group-legacy'));
|
||||||
|
|
||||||
|
groupsArray.forEach((group) => {
|
||||||
|
if (collapsed.indexOf(group.id) !== -1) {
|
||||||
|
expandCollapseToggle(group, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Necessary in order to not fire the transition animation on page load, which looks wrong.
|
||||||
|
// The timeout isn't doing anything visible to the user, so it's not making the page load look slower.
|
||||||
|
setTimeout(() => {
|
||||||
|
groupsArray.forEach((group) => {
|
||||||
|
const container = group.querySelector('.feature-group-container');
|
||||||
|
container.classList.add('animated-group');
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll('.feature-group-header')).forEach((header) => {
|
||||||
|
const parent = header.parentNode;
|
||||||
|
header.onclick = () => {
|
||||||
|
expandCollapseToggle(parent);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
showFavoritesOnly();
|
||||||
|
});
|
@ -55,6 +55,10 @@ hideCookieBanner();
|
|||||||
updateFavoriteIcons();
|
updateFavoriteIcons();
|
||||||
const contentPath = /*[[${@contextPath}]]*/ '';
|
const contentPath = /*[[${@contextPath}]]*/ '';
|
||||||
|
|
||||||
|
const defaultView = localStorage.getItem('defaultView') || 'home'; // Default to "home"
|
||||||
|
if (defaultView === 'home-legacy') {
|
||||||
|
window.location.href = contentPath + 'home-legacy'; // Redirect to legacy view
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const surveyVersion = '3.0';
|
const surveyVersion = '3.0';
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
<div th:fragment="featureGroupHeader" class="feature-group-header">
|
||||||
|
<h3 class="menu-title" th:text="${groupTitle}"></h3>
|
||||||
|
<span class="material-symbols-rounded header-expand-button">
|
||||||
|
chevron_right
|
||||||
|
</span>
|
||||||
|
</div>
|
528
src/main/resources/templates/home-legacy.html
Normal file
528
src/main/resources/templates/home-legacy.html
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title='')}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<!-- Jumbotron -->
|
||||||
|
<div class="p-5 rounded d-none d-md-block" id="jumbotron">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="display-4 fw-normal" th:text="${@appName}"></h1>
|
||||||
|
<p class="lead fs-4"
|
||||||
|
th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<br class="d-md-none">
|
||||||
|
<!-- Features -->
|
||||||
|
<script th:src="@{'/js/homecard-legacy.js'}"></script>
|
||||||
|
<div class=" container">
|
||||||
|
<br>
|
||||||
|
<span class="material-symbols-rounded search-icon">
|
||||||
|
search
|
||||||
|
</span>
|
||||||
|
<input type="text" id="searchBar" onkeyup="filterCardsLegacy()" th:placeholder="#{home.searchBar}" autofocus>
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<a href="home" onclick="setAsDefault('home')"
|
||||||
|
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
||||||
|
<span th:text="#{home.newHomePage}">
|
||||||
|
</span>
|
||||||
|
<span class="material-symbols-rounded toggle-favourites" style="font-size: 2rem; margin-left: 0.2rem;">
|
||||||
|
home
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="filtersContainer">
|
||||||
|
<span class="material-symbols-rounded filter-button" onclick="toggleFavoritesOnly()">
|
||||||
|
star
|
||||||
|
</span>
|
||||||
|
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(true)">
|
||||||
|
expand_all
|
||||||
|
</span>
|
||||||
|
<span class="material-symbols-rounded filter-button" onclick="expandCollapseAll(false)">
|
||||||
|
collapse_all
|
||||||
|
</span>
|
||||||
|
<span class="material-symbols-rounded filter-button hidden" onclick="switchViewMode()">
|
||||||
|
dashboard
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features-container">
|
||||||
|
|
||||||
|
<div th:if="${@shouldShow}" class="feature-card favorite update-notice visually-hidden" id="update-link-legacy">
|
||||||
|
<a href="https://github.com/Stirling-Tools/Stirling-PDF/releases" target="_blank" rel="noopener">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div id="tool-icon" class="advance" alt="icon">
|
||||||
|
<span class="material-symbols-rounded nav-icon">update</span>
|
||||||
|
</div>
|
||||||
|
<div id="tool-text">
|
||||||
|
<h5 class="card-title" th:text="#{settings.update}"></h5>
|
||||||
|
<p class="card-text" id="app-update"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupFavorites" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.favorite})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div id="popularTools" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.popular})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div id="groupOrganize" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.organize})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', toolIcon='construction', tags=#{multiTool.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', toolIcon='add_to_photos', tags=#{merge.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', toolIcon='cut', tags=#{split.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', toolIcon='rotate_right', tags=#{rotate.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', toolIcon='crop', tags=#{crop.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', toolIcon='format_list_bulleted', tags=#{pdfOrganiser.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', toolIcon='delete', tags=#{removePages.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', toolIcon='dashboard', tags=#{pageLayout.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', toolIcon='fullscreen', tags=#{scalePages.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', toolIcon='upload', tags=#{extractPage.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', toolIcon='looks_one', tags=#{PdfToSinglePage.tags}, toolGroup='organize')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupConvertTo" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertTo})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', toolIcon='picture_as_pdf', tags=#{imageToPdf.tags}, toolGroup='image')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', toolIcon='draft', tags=#{fileToPDF.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', toolIcon='link', tags=#{URLToPDF.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', toolIcon='html', tags=#{HTMLToPDF.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', toolIcon='markdown', tags=#{MarkdownToPDF.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupConvertFrom" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.convertFrom})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', toolIcon='photo_library', tags=#{pdfToImage.tags}, toolGroup='image')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', toolIcon='picture_as_pdf', tags=#{pdfToPDFA.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', toolIcon='description', tags=#{PDFToWord.tags}, toolGroup='word')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', toolIcon='slideshow', tags=#{PDFToPresentation.tags}, toolGroup='ppt')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', toolIcon='text_fields', tags=#{PDFToText.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', toolIcon='html', tags=#{PDFToHTML.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', toolIcon='code', tags=#{PDFToXML.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', toolIcon='csv', tags=#{tableExtraxt.tags}, toolGroup='convert')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupSecurity" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.security})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', toolIcon='lock', tags=#{addPassword.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', toolIcon='lock_open_right', tags=#{removePassword.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', toolIcon='encrypted', tags=#{permissions.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', toolIcon='signature', tags=#{sign.tags}, toolGroup='sign')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', toolIcon='workspace_premium', tags=#{certSign.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='validate-signature', cardTitle=#{home.validateSignature.title}, cardText=#{home.validateSignature.desc}, cardLink='validate-signature', toolIcon='verified', tags=#{validateSignature.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-cert-sign', cardTitle=#{home.removeCertSign.title}, cardText=#{home.removeCertSign.desc}, cardLink='remove-cert-sign', toolIcon='remove_moderator', tags=#{removeCertSign.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', toolIcon='sanitizer', tags=#{sanitizePdf.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', toolIcon='ink_eraser', tags=#{autoRedact.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='redact', cardTitle=#{home.redact.title}, cardText=#{home.redact.desc}, cardLink='redact', toolIcon='playlist_remove', tags=#{redact.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='stamp', cardTitle=#{home.AddStampRequest.title}, cardText=#{home.AddStampRequest.desc}, cardLink='stamp', toolIcon='approval', tags=#{AddStampRequest.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', toolIcon='water_drop', tags=#{watermark.tags}, toolGroup='security')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="groupView" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.edit})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', toolIcon='menu_book', tags=#{viewPdf.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', toolIcon='123', tags=#{add-page-numbers.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', toolIcon='add_photo_alternate', tags=#{addImage.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', toolIcon='assignment', tags=#{changeMetadata.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', toolIcon='quick_reference_all', tags=#{ocr.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', toolIcon='wallpaper', tags=#{extractImages.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', toolIcon='layers_clear', tags=#{flatten.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', toolIcon='scan_delete', tags=#{removeBlanks.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-annotations', cardTitle=#{home.removeAnnotations.title}, cardText=#{home.removeAnnotations.desc}, cardLink='remove-annotations', toolIcon='thread_unread', tags=#{removeAnnotations.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', toolIcon='compare', tags=#{compare.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', toolIcon='info', tags=#{getPdfInfo.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='remove-image-pdf', cardTitle=#{home.removeImagePdf.title}, cardText=#{home.removeImagePdf.desc}, cardLink='remove-image-pdf', toolIcon='remove_selection', tags=#{removeImagePdf.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='replace-and-invert-color-pdf', cardTitle=#{home.replaceColorPdf.title}, cardText=#{home.replaceColorPdf.desc}, cardLink='replace-and-invert-color-pdf', toolIcon='format_color_fill', tags=#{replaceColorPdf.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="groupAdvanced" class="feature-group-legacy">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/featureGroupHeaderLegacy :: featureGroupHeader(groupTitle=#{navbar.sections.advance})}">
|
||||||
|
</div>
|
||||||
|
<div class="feature-group-container">
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', toolIcon='family_history', tags=#{pipeline.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', toolIcon='palette', tags=#{adjust-contrast.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', toolIcon='zoom_in_map', tags=#{compressPdfs.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', toolIcon='scanner', tags=#{ScannerImageSplit.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', toolIcon='build', tags=#{repair.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', toolIcon='text_fields_alt', tags=#{auto-rename.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', toolIcon='cut', tags=#{autoSplitPDF.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', toolIcon='javascript', tags=#{showJS.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='split-by-size-or-count', cardTitle=#{home.autoSizeSplitPDF.title}, cardText=#{home.autoSizeSplitPDF.desc}, cardLink='split-by-size-or-count', toolIcon='vertical_split', tags=#{autoSizeSplitPDF.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='overlay-pdf', cardTitle=#{home.overlay-pdfs.title}, cardText=#{home.overlay-pdfs.desc}, cardLink='overlay-pdf', toolIcon='layers', tags=#{overlay-pdfs.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='split-pdf-by-sections', cardTitle=#{home.split-by-sections.title}, cardText=#{home.split-by-sections.desc}, cardLink='split-pdf-by-sections', toolIcon='grid_on', tags=#{split-by-sections.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/card :: card(id='split-pdf-by-chapters', cardTitle=#{home.splitPdfByChapters.title}, cardText=#{home.splitPdfByChapters.desc}, cardLink='split-pdf-by-chapters', toolIcon='book', tags=#{splitPdfByChapters.tags}, toolGroup='advance')}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Survey Modal -->
|
||||||
|
<div class="modal fade" id="surveyModal" tabindex="-1" role="dialog" aria-labelledby="surveyModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="surveyModalLabel" th:text="#{survey.title}">Stirling-PDF Survey</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p th:text="#{survey.meeting.1}">If you're using Stirling PDF at work, we'd love to speak to you. We're offering free technical support in exchange for a 15 minute user discovery session.</p>
|
||||||
|
<p th:text="#{survey.meeting.2}">This is a chance to:</p>
|
||||||
|
<p><span>🛠️</span><span th:text="#{survey.meeting.3}">Get help with deployment, integrations, or troubleshooting</span></p>
|
||||||
|
<p><span>📢</span><span th:text="#{survey.meeting.4}">Provide direct feedback on performance, edge cases, and feature gaps</span></p>
|
||||||
|
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
|
||||||
|
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
|
||||||
|
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
|
||||||
|
<a href="https://calendly.com/d/cm4p-zz5-yy8/stirling-pdf-15-minute-group-discussion" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
|
||||||
|
</br>
|
||||||
|
</br>
|
||||||
|
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
|
||||||
|
|
||||||
|
<p th:text="#{survey.please}">Please consider taking our survey!</p>
|
||||||
|
<a href="https://stirlingpdf.info/s/cm28y3niq000o56dv7liv8wsu" target="_blank" class="btn btn-primary"
|
||||||
|
id="takeSurvey" th:text="#{survey.button}">Take Survey</a>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input type="checkbox" id="dontShowAgain">
|
||||||
|
<label for="dontShowAgain" th:text="#{survey.dontShowAgain}">Don't show again</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}">Close</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Analytics Modal -->
|
||||||
|
<div class="modal fade" id="analyticsModal" tabindex="-1" role="dialog" aria-labelledby="analyticsModalLabel"
|
||||||
|
aria-hidden="true" th:if="${@analyticsPrompt}">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="analyticsModalLabel" th:text="#{analytics.title}">Do you want make Stirling PDF
|
||||||
|
better?</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p th:text="#{analytics.paragraph1}">Stirling PDF has opt in analytics to help us improve the product. We do
|
||||||
|
not track any personal information or file contents.</p>
|
||||||
|
<p th:text="#{analytics.paragraph2}">Please consider enabling analytics to help Stirling-PDF grow and to allow
|
||||||
|
us to understand our users better.</p>
|
||||||
|
<p th:text="#{analytics.settings}">You can change the settings for analytics in the config/settings.yml file
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-between">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="setAnalytics(false)"
|
||||||
|
th:text="#{analytics.disable}">Disable analytics</button>
|
||||||
|
<button type="button" class="btn btn-primary" th:text="#{analytics.enable}"
|
||||||
|
onclick="setAnalytics(true)">Enable analytics</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script th:src="@{'/js/fetch-utils.js'}"></script>
|
||||||
|
<script th:inline="javascript">
|
||||||
|
|
||||||
|
/*<![CDATA[*/
|
||||||
|
const analyticsPromptBoolean = /*[[${@analyticsPrompt}]]*/ false;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
if (analyticsPromptBoolean) {
|
||||||
|
const analyticsModal = new bootstrap.Modal(document.getElementById('analyticsModal'));
|
||||||
|
analyticsModal.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*]]>*/
|
||||||
|
function setAnalytics(enabled) {
|
||||||
|
fetchWithCsrf('api/v1/settings/update-enable-analytics', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(enabled)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
console.log('Analytics setting updated successfully');
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('analyticsModal')).hide();
|
||||||
|
} else if (response.status === 208) {
|
||||||
|
console.log('Analytics setting has already been set. Please edit /config/settings.yml to change it.', response);
|
||||||
|
alert('Analytics setting has already been set. Please edit /config/settings.yml to change it.');
|
||||||
|
} else {
|
||||||
|
throw new Error('Unexpected response status: ' + response.status);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error updating analytics setting:', error);
|
||||||
|
alert('An error occurred while updating the analytics setting. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const surveyVersion = "3.0";
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('surveyModal'));
|
||||||
|
const dontShowAgain = document.getElementById('dontShowAgain');
|
||||||
|
const takeSurveyButton = document.getElementById('takeSurvey');
|
||||||
|
|
||||||
|
const viewThresholds = [5, 10, 15, 22, 30, 50, 75, 100, 150, 200];
|
||||||
|
|
||||||
|
// Check if survey version changed and reset page views if it did
|
||||||
|
const storedVersion = localStorage.getItem('surveyVersion');
|
||||||
|
if (storedVersion && storedVersion !== surveyVersion) {
|
||||||
|
localStorage.setItem('pageViews', '0');
|
||||||
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pageViews = parseInt(localStorage.getItem('pageViews') || '0');
|
||||||
|
|
||||||
|
pageViews++;
|
||||||
|
localStorage.setItem('pageViews', pageViews.toString());
|
||||||
|
|
||||||
|
function shouldShowSurvey() {
|
||||||
|
if (localStorage.getItem('dontShowSurvey') === 'true' ||
|
||||||
|
localStorage.getItem('surveyTaken') === 'true') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If survey version changed and we hit a threshold, show the survey
|
||||||
|
if (localStorage.getItem('surveyVersion') !== surveyVersion &&
|
||||||
|
viewThresholds.includes(pageViews)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewThresholds.includes(pageViews);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldShowSurvey()) {
|
||||||
|
modal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
dontShowAgain.addEventListener('change', function () {
|
||||||
|
if (this.checked) {
|
||||||
|
localStorage.setItem('dontShowSurvey', 'true');
|
||||||
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem('dontShowSurvey');
|
||||||
|
localStorage.removeItem('surveyVersion');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
takeSurveyButton.addEventListener('click', function () {
|
||||||
|
localStorage.setItem('surveyTaken', 'true');
|
||||||
|
localStorage.setItem('surveyVersion', surveyVersion);
|
||||||
|
modal.hide();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (localStorage.getItem('dontShowSurvey')) {
|
||||||
|
modal.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function setAsDefault(value) {
|
||||||
|
localStorage.setItem('defaultView', value);
|
||||||
|
console.log(`Default view set to: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -82,6 +82,13 @@
|
|||||||
visibility
|
visibility
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<a href="home" onclick="setAsDefault('home-legacy')" th:title="#{home.legacyHomepage}"
|
||||||
|
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
||||||
|
<span class="material-symbols-rounded toggle-favourites"
|
||||||
|
style="font-size: 2rem; margin-left: 0.2rem;">
|
||||||
|
home
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
<a th:if="${@shouldShow}" href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
<a th:if="${@shouldShow}" href="https://github.com/Stirling-Tools/Stirling-PDF/releases"
|
||||||
target="_blank" id="update-link" rel="noopener" th:title="#{settings.update}"
|
target="_blank" id="update-link" rel="noopener" th:title="#{settings.update}"
|
||||||
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
style="text-decoration: none; color: inherit; cursor: pointer; display: flex; align-items: center;">
|
||||||
@ -138,7 +145,7 @@
|
|||||||
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
|
<p><span>🔍</span><span th:text="#{survey.meeting.5}">Help us refine Stirling PDF for real-world enterprise use</span></p>
|
||||||
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
|
<p th:text="#{survey.meeting.6}">If you're interested, you can book time with our team directly.</p>
|
||||||
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
|
<p th:text="#{survey.meeting.7}">Looking forward to digging into your use cases and making Stirling PDF even better!</p>
|
||||||
<a href="https://calendly.com/d/crsr-tz6-487" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
|
<a href="https://calendly.com/d/cm4p-zz5-yy8/stirling-pdf-15-minute-group-discussion" target="_blank" class="btn btn-primary" id="takeSurvey2" th:text="#{survey.meeting.button}">Book meeting</a>
|
||||||
</br>
|
</br>
|
||||||
</br>
|
</br>
|
||||||
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
|
<p th:text="#{survey.meeting.notInterested}">Not a business and/or interested in a meeting?</p>
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
package stirling.software.SPDF.EE;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class LicenseKeyCheckerTest {
|
|
||||||
|
|
||||||
@Mock private KeygenLicenseVerifier verifier;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void premiumDisabled_skipsVerification() {
|
|
||||||
ApplicationProperties props = new ApplicationProperties();
|
|
||||||
props.getPremium().setEnabled(false);
|
|
||||||
props.getPremium().setKey("dummy");
|
|
||||||
|
|
||||||
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
|
|
||||||
|
|
||||||
assertEquals(License.NORMAL, checker.getPremiumLicenseEnabledResult());
|
|
||||||
verifyNoInteractions(verifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void directKey_verified() {
|
|
||||||
ApplicationProperties props = new ApplicationProperties();
|
|
||||||
props.getPremium().setEnabled(true);
|
|
||||||
props.getPremium().setKey("abc");
|
|
||||||
when(verifier.verifyLicense("abc")).thenReturn(License.PRO);
|
|
||||||
|
|
||||||
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
|
|
||||||
|
|
||||||
assertEquals(License.PRO, checker.getPremiumLicenseEnabledResult());
|
|
||||||
verify(verifier).verifyLicense("abc");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void fileKey_verified(@TempDir Path temp) throws IOException {
|
|
||||||
Path file = temp.resolve("license.txt");
|
|
||||||
Files.writeString(file, "filekey");
|
|
||||||
|
|
||||||
ApplicationProperties props = new ApplicationProperties();
|
|
||||||
props.getPremium().setEnabled(true);
|
|
||||||
props.getPremium().setKey("file:" + file.toString());
|
|
||||||
when(verifier.verifyLicense("filekey")).thenReturn(License.ENTERPRISE);
|
|
||||||
|
|
||||||
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
|
|
||||||
|
|
||||||
assertEquals(License.ENTERPRISE, checker.getPremiumLicenseEnabledResult());
|
|
||||||
verify(verifier).verifyLicense("filekey");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void missingFile_resultsNormal(@TempDir Path temp) {
|
|
||||||
Path file = temp.resolve("missing.txt");
|
|
||||||
ApplicationProperties props = new ApplicationProperties();
|
|
||||||
props.getPremium().setEnabled(true);
|
|
||||||
props.getPremium().setKey("file:" + file.toString());
|
|
||||||
|
|
||||||
LicenseKeyChecker checker = new LicenseKeyChecker(verifier, props);
|
|
||||||
|
|
||||||
assertEquals(License.NORMAL, checker.getPremiumLicenseEnabledResult());
|
|
||||||
verifyNoInteractions(verifier);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package stirling.software.SPDF.controller.api.pipeline;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
import static org.mockito.ArgumentMatchers.*;
|
|
||||||
import static org.mockito.Mockito.*;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
|
||||||
import org.mockito.Mock;
|
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
|
||||||
import org.springframework.core.io.ByteArrayResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
|
||||||
|
|
||||||
import jakarta.servlet.ServletContext;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.model.PipelineConfig;
|
|
||||||
import stirling.software.SPDF.model.PipelineOperation;
|
|
||||||
import stirling.software.SPDF.model.PipelineResult;
|
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
|
||||||
class PipelineProcessorTest {
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
ApiDocService apiDocService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
UserServiceInterface userService;
|
|
||||||
|
|
||||||
@Mock
|
|
||||||
ServletContext servletContext;
|
|
||||||
|
|
||||||
PipelineProcessor pipelineProcessor;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void setUp() {
|
|
||||||
pipelineProcessor = spy(new PipelineProcessor(apiDocService, userService, servletContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void runPipelineWithFilterSetsFlag() throws Exception {
|
|
||||||
PipelineOperation op = new PipelineOperation();
|
|
||||||
op.setOperation("filter-page-count");
|
|
||||||
op.setParameters(Map.of());
|
|
||||||
PipelineConfig config = new PipelineConfig();
|
|
||||||
config.setOperations(List.of(op));
|
|
||||||
|
|
||||||
Resource file = new ByteArrayResource("data".getBytes()) {
|
|
||||||
@Override
|
|
||||||
public String getFilename() {
|
|
||||||
return "test.pdf";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
List<Resource> files = List.of(file);
|
|
||||||
|
|
||||||
when(apiDocService.isMultiInput("filter-page-count")).thenReturn(false);
|
|
||||||
when(apiDocService.getExtensionTypes(false, "filter-page-count")).thenReturn(List.of("pdf"));
|
|
||||||
|
|
||||||
doReturn(new ResponseEntity<>(new byte[0], HttpStatus.OK))
|
|
||||||
.when(pipelineProcessor)
|
|
||||||
.sendWebRequest(anyString(), any());
|
|
||||||
|
|
||||||
PipelineResult result = pipelineProcessor.runPipelineAgainstFiles(files, config);
|
|
||||||
|
|
||||||
assertTrue(result.isFiltersApplied(), "Filter flag should be true when operation filters file");
|
|
||||||
assertFalse(result.isHasErrors(), "No errors should occur");
|
|
||||||
assertTrue(result.getOutputFiles().isEmpty(), "Filtered file list should be empty");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
|||||||
package stirling.software.SPDF.service;
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.Loader;
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdmodel.*;
|
import org.apache.pdfbox.pdmodel.*;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDStream;
|
import org.apache.pdfbox.pdmodel.common.PDStream;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.junit.jupiter.api.*;
|
import org.junit.jupiter.api.*;
|
||||||
import org.junit.jupiter.api.parallel.Execution;
|
import org.junit.jupiter.api.parallel.Execution;
|
||||||
import org.junit.jupiter.api.parallel.ExecutionMode;
|
import org.junit.jupiter.api.parallel.ExecutionMode;
|
||||||
@ -42,7 +43,12 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
@CsvSource({
|
||||||
|
"5,MEMORY_ONLY",
|
||||||
|
"20,MIXED",
|
||||||
|
"60,TEMP_FILE"
|
||||||
|
|
||||||
|
})
|
||||||
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_FileInput(int sizeMB, StrategyType expected) throws IOException {
|
||||||
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
File file = writeTempFile(inflatePdf(basePdfBytes, sizeMB));
|
||||||
try (PDDocument doc = factory.load(file)) {
|
try (PDDocument doc = factory.load(file)) {
|
||||||
@ -51,7 +57,12 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
@CsvSource({
|
||||||
|
"5,MEMORY_ONLY",
|
||||||
|
"20,MIXED",
|
||||||
|
"60,TEMP_FILE"
|
||||||
|
|
||||||
|
})
|
||||||
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_ByteArray(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
try (PDDocument doc = factory.load(inflated)) {
|
try (PDDocument doc = factory.load(inflated)) {
|
||||||
@ -60,7 +71,12 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
@CsvSource({
|
||||||
|
"5,MEMORY_ONLY",
|
||||||
|
"20,MIXED",
|
||||||
|
"60,TEMP_FILE"
|
||||||
|
|
||||||
|
})
|
||||||
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_InputStream(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
try (PDDocument doc = factory.load(new ByteArrayInputStream(inflated))) {
|
||||||
@ -69,22 +85,30 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
@CsvSource({
|
||||||
|
"5,MEMORY_ONLY",
|
||||||
|
"20,MIXED",
|
||||||
|
"60,TEMP_FILE"
|
||||||
|
|
||||||
|
})
|
||||||
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_MultipartFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart =
|
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
|
||||||
try (PDDocument doc = factory.load(multipart)) {
|
try (PDDocument doc = factory.load(multipart)) {
|
||||||
assertEquals(expected, factory.lastStrategyUsed);
|
assertEquals(expected, factory.lastStrategyUsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@CsvSource({"5,MEMORY_ONLY", "20,MIXED", "60,TEMP_FILE"})
|
@CsvSource({
|
||||||
|
"5,MEMORY_ONLY",
|
||||||
|
"20,MIXED",
|
||||||
|
"60,TEMP_FILE"
|
||||||
|
|
||||||
|
})
|
||||||
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
void testStrategy_PDFFile(int sizeMB, StrategyType expected) throws IOException {
|
||||||
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
byte[] inflated = inflatePdf(basePdfBytes, sizeMB);
|
||||||
MockMultipartFile multipart =
|
MockMultipartFile multipart = new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
||||||
new MockMultipartFile("file", "doc.pdf", "application/pdf", inflated);
|
|
||||||
PDFFile pdfFile = new PDFFile();
|
PDFFile pdfFile = new PDFFile();
|
||||||
pdfFile.setFileInput(multipart);
|
pdfFile.setFileInput(multipart);
|
||||||
try (PDDocument doc = factory.load(pdfFile)) {
|
try (PDDocument doc = factory.load(pdfFile)) {
|
||||||
@ -101,9 +125,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
|
stream.getCOSObject().setItem(COSName.TYPE, COSName.XOBJECT);
|
||||||
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
|
stream.getCOSObject().setItem(COSName.SUBTYPE, COSName.IMAGE);
|
||||||
|
|
||||||
doc.getDocumentCatalog()
|
doc.getDocumentCatalog().getCOSObject().setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
|
||||||
.getCOSObject()
|
|
||||||
.setItem(COSName.getPDFName("DummyBigStream"), stream.getCOSObject());
|
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
doc.save(out);
|
doc.save(out);
|
||||||
@ -144,14 +166,14 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
// try (InputStream is = getClass().getResourceAsStream("/protected.pdf")) {
|
||||||
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
// assertNotNull(is, "protected.pdf must be present in src/test/resources");
|
||||||
// byte[] bytes = is.readAllBytes();
|
// byte[] bytes = is.readAllBytes();
|
||||||
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf",
|
// MockMultipartFile file = new MockMultipartFile("file", "protected.pdf", "application/pdf", bytes);
|
||||||
// "application/pdf", bytes);
|
|
||||||
// try (PDDocument doc = factory.load(file, "test123")) {
|
// try (PDDocument doc = factory.load(file, "test123")) {
|
||||||
// assertNotNull(doc);
|
// assertNotNull(doc);
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testLoadReadOnlySkipsPostProcessing() throws IOException {
|
void testLoadReadOnlySkipsPostProcessing() throws IOException {
|
||||||
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
PdfMetadataService mockService = mock(PdfMetadataService.class);
|
||||||
@ -164,6 +186,7 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testCreateNewDocument() throws IOException {
|
void testCreateNewDocument() throws IOException {
|
||||||
try (PDDocument doc = factory.createNewDocument()) {
|
try (PDDocument doc = factory.createNewDocument()) {
|
||||||
@ -220,4 +243,5 @@ class CustomPDFDocumentFactoryTest {
|
|||||||
void cleanup() {
|
void cleanup() {
|
||||||
System.gc();
|
System.gc();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
package stirling.software.SPDF.service;
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
import org.apache.pdfbox.io.RandomAccessStreamCache.StreamCacheCreateFunction;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.SPDF.service.PdfMetadataService;
|
||||||
|
|
||||||
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
class SpyPDFDocumentFactory extends CustomPDFDocumentFactory {
|
||||||
enum StrategyType {
|
enum StrategyType {
|
||||||
MEMORY_ONLY,
|
MEMORY_ONLY, MIXED, TEMP_FILE
|
||||||
MIXED,
|
|
||||||
TEMP_FILE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public StrategyType lastStrategyUsed;
|
public StrategyType lastStrategyUsed;
|
||||||
|
@ -29,26 +29,23 @@ class CustomHtmlSanitizerTest {
|
|||||||
return Stream.of(
|
return Stream.of(
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
"<p>This is <strong>valid</strong> HTML with <em>formatting</em>.</p>",
|
||||||
new String[] {"<p>", "<strong>", "<em>"}),
|
new String[] {"<p>", "<strong>", "<em>"}
|
||||||
|
),
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
"<p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>, "
|
||||||
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
+ "<em>emphasis</em>, <strong>strong</strong>, <strike>strikethrough</strike>, "
|
||||||
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
+ "<s>strike</s>, <sub>subscript</sub>, <sup>superscript</sup>, "
|
||||||
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
+ "<tt>teletype</tt>, <code>code</code>, <big>big</big>, <small>small</small>.</p>",
|
||||||
new String[] {
|
new String[] {"<b>bold</b>", "<i>italic</i>", "<em>emphasis</em>", "<strong>strong</strong>"}
|
||||||
"<b>bold</b>",
|
),
|
||||||
"<i>italic</i>",
|
|
||||||
"<em>emphasis</em>",
|
|
||||||
"<strong>strong</strong>"
|
|
||||||
}),
|
|
||||||
Arguments.of(
|
Arguments.of(
|
||||||
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
"<div>Division</div><h1>Heading 1</h1><h2>Heading 2</h2><h3>Heading 3</h3>"
|
||||||
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
+ "<h4>Heading 4</h4><h5>Heading 5</h5><h6>Heading 6</h6>"
|
||||||
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
|
+ "<blockquote>Blockquote</blockquote><ul><li>List item</li></ul>"
|
||||||
+ "<ol><li>Ordered item</li></ol>",
|
+ "<ol><li>Ordered item</li></ol>",
|
||||||
new String[] {
|
new String[] {"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"}
|
||||||
"<div>", "<h1>", "<h6>", "<blockquote>", "<ul>", "<ol>", "<li>"
|
)
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
package stirling.software.SPDF.utils;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
class GeneralUtilsAdditionalTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testConvertSizeToBytes() {
|
|
||||||
assertEquals(1024L, GeneralUtils.convertSizeToBytes("1KB"));
|
|
||||||
assertEquals(1024L * 1024, GeneralUtils.convertSizeToBytes("1MB"));
|
|
||||||
assertEquals(1024L * 1024 * 1024, GeneralUtils.convertSizeToBytes("1GB"));
|
|
||||||
assertEquals(100L * 1024 * 1024, GeneralUtils.convertSizeToBytes("100"));
|
|
||||||
assertNull(GeneralUtils.convertSizeToBytes("invalid"));
|
|
||||||
assertNull(GeneralUtils.convertSizeToBytes(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testFormatBytes() {
|
|
||||||
assertEquals("512 B", GeneralUtils.formatBytes(512));
|
|
||||||
assertEquals("1.00 KB", GeneralUtils.formatBytes(1024));
|
|
||||||
assertEquals("1.00 MB", GeneralUtils.formatBytes(1024L * 1024));
|
|
||||||
assertEquals("1.00 GB", GeneralUtils.formatBytes(1024L * 1024 * 1024));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testURLHelpersAndUUID() {
|
|
||||||
assertTrue(GeneralUtils.isValidURL("https://example.com"));
|
|
||||||
assertFalse(GeneralUtils.isValidURL("htp:/bad"));
|
|
||||||
assertFalse(GeneralUtils.isURLReachable("http://localhost"));
|
|
||||||
assertFalse(GeneralUtils.isURLReachable("ftp://example.com"));
|
|
||||||
|
|
||||||
assertTrue(GeneralUtils.isValidUUID("123e4567-e89b-12d3-a456-426614174000"));
|
|
||||||
assertFalse(GeneralUtils.isValidUUID("not-a-uuid"));
|
|
||||||
|
|
||||||
assertFalse(GeneralUtils.isVersionHigher(null, "1.0"));
|
|
||||||
assertTrue(GeneralUtils.isVersionHigher("2.0", "1.9"));
|
|
||||||
assertFalse(GeneralUtils.isVersionHigher("1.0", "1.0.1"));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
package stirling.software.SPDF.utils;
|
package stirling.software.SPDF.utils;
|
||||||
|
|
||||||
|
import io.github.pixee.security.ZipSecurity;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@ -28,8 +29,6 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.mock.web.MockMultipartFile;
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import io.github.pixee.security.ZipSecurity;
|
|
||||||
|
|
||||||
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -215,8 +214,7 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(
|
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMdFiles = false;
|
boolean foundMdFiles = false;
|
||||||
boolean foundImage = false;
|
boolean foundImage = false;
|
||||||
@ -288,8 +286,7 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(
|
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMainHtml = false;
|
boolean foundMainHtml = false;
|
||||||
boolean foundIndexHtml = false;
|
boolean foundIndexHtml = false;
|
||||||
@ -440,8 +437,7 @@ class PDFToFileTest {
|
|||||||
|
|
||||||
// Verify the content by unzipping it
|
// Verify the content by unzipping it
|
||||||
try (ZipInputStream zipStream =
|
try (ZipInputStream zipStream =
|
||||||
ZipSecurity.createHardenedInputStream(
|
ZipSecurity.createHardenedInputStream(new java.io.ByteArrayInputStream(response.getBody()))) {
|
||||||
new java.io.ByteArrayInputStream(response.getBody()))) {
|
|
||||||
ZipEntry entry;
|
ZipEntry entry;
|
||||||
boolean foundMainFile = false;
|
boolean foundMainFile = false;
|
||||||
boolean foundMediaFiles = false;
|
boolean foundMediaFiles = false;
|
||||||
|
@ -5,17 +5,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
|
|||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
import org.apache.pdfbox.cos.COSName;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
import org.apache.pdfbox.pdmodel.PDResources;
|
import org.apache.pdfbox.pdmodel.PDResources;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
@ -23,10 +18,6 @@ import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.Mockito;
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
|
||||||
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
|
|
||||||
import stirling.software.SPDF.service.PdfMetadataService;
|
|
||||||
|
|
||||||
public class PdfUtilsTest {
|
public class PdfUtilsTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -58,68 +49,4 @@ public class PdfUtilsTest {
|
|||||||
|
|
||||||
assertTrue(PdfUtils.hasImagesOnPage(page));
|
assertTrue(PdfUtils.hasImagesOnPage(page));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPageCountComparators() throws Exception {
|
|
||||||
PDDocument doc1 = new PDDocument();
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
doc1.addPage(new PDPage());
|
|
||||||
PdfUtils utils = new PdfUtils();
|
|
||||||
assertTrue(utils.pageCount(doc1, 2, "greater"));
|
|
||||||
|
|
||||||
PDDocument doc2 = new PDDocument();
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
doc2.addPage(new PDPage());
|
|
||||||
assertTrue(utils.pageCount(doc2, 3, "equal"));
|
|
||||||
|
|
||||||
PDDocument doc3 = new PDDocument();
|
|
||||||
doc3.addPage(new PDPage());
|
|
||||||
doc3.addPage(new PDPage());
|
|
||||||
assertTrue(utils.pageCount(doc3, 5, "less"));
|
|
||||||
|
|
||||||
PDDocument doc4 = new PDDocument();
|
|
||||||
doc4.addPage(new PDPage());
|
|
||||||
assertThrows(IllegalArgumentException.class, () -> utils.pageCount(doc4, 1, "bad"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testPageSize() throws Exception {
|
|
||||||
PDDocument doc = new PDDocument();
|
|
||||||
PDPage page = new PDPage(PDRectangle.A4);
|
|
||||||
doc.addPage(page);
|
|
||||||
PDRectangle rect = page.getMediaBox();
|
|
||||||
String expected = rect.getWidth() + "x" + rect.getHeight();
|
|
||||||
PdfUtils utils = new PdfUtils();
|
|
||||||
assertTrue(utils.pageSize(doc, expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void testOverlayImage() throws Exception {
|
|
||||||
PDDocument doc = new PDDocument();
|
|
||||||
doc.addPage(new PDPage(PDRectangle.A4));
|
|
||||||
ByteArrayOutputStream pdfOut = new ByteArrayOutputStream();
|
|
||||||
doc.save(pdfOut);
|
|
||||||
doc.close();
|
|
||||||
|
|
||||||
BufferedImage image = new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB);
|
|
||||||
Graphics2D g = image.createGraphics();
|
|
||||||
g.setColor(Color.RED);
|
|
||||||
g.fillRect(0, 0, 10, 10);
|
|
||||||
g.dispose();
|
|
||||||
ByteArrayOutputStream imgOut = new ByteArrayOutputStream();
|
|
||||||
javax.imageio.ImageIO.write(image, "png", imgOut);
|
|
||||||
|
|
||||||
PdfMetadataService meta =
|
|
||||||
new PdfMetadataService(new ApplicationProperties(), "label", false, null);
|
|
||||||
CustomPDFDocumentFactory factory = new CustomPDFDocumentFactory(meta);
|
|
||||||
|
|
||||||
byte[] result =
|
|
||||||
PdfUtils.overlayImage(
|
|
||||||
factory, pdfOut.toByteArray(), imgOut.toByteArray(), 0, 0, false);
|
|
||||||
try (PDDocument resultDoc = factory.load(result)) {
|
|
||||||
assertEquals(1, resultDoc.getNumberOfPages());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user