Compare commits

..

No commits in common. "main" and "v0.43.1" have entirely different histories.

269 changed files with 4612 additions and 11763 deletions

View File

@ -1,131 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-dockerfile
{
"name": "Stirling-PDF Dev Container",
"build": {
// Sets the run context to one level up instead of the .devcontainer folder.
"context": "..",
// Update the 'dockerFile' property if you aren't using the standard 'Dockerfile' filename.
"dockerfile": "../Dockerfile.dev"
},
"runArgs": [
"-e",
"GIT_EDITOR=code --wait",
"--security-opt",
"label=disable"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [8080, 2002, 2003],
"portsAttributes": {
"8080": {
"label": "Stirling-PDF Dev Port"
},
"2002": {
"label": "unoserver Port"
},
"2003": {
"label": "UnoConvert Port"
}
},
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
"mounts": [
"source=logs-volume,target=/workspace/logs,type=volume",
"source=build-volume,target=/workspace/build,type=volume"
],
"workspaceFolder": "/workspace",
// Configure tool-specific properties.
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"editor.wordSegmenterLocales": "",
"editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active",
"cSpell.enabled": false,
"[java]": {
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
},
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive",
"java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle",
"java.format.settings.google.version": "1.26.0",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
"java.saveActions.cleanup": true,
"java.cleanup.actions": [
"invertEquals",
"instanceofPatternMatch"
],
"java.completion.engine": "dom",
"java.completion.enabled": true,
"java.completion.importOrder": [
"java",
"javax",
"org",
"com",
"net",
"io",
"jakarta",
"lombok",
"me",
"stirling"
],
"java.project.resourceFilters": [
".devcontainer/",
".git/",
".github/",
".gradle/",
".venv/",
".venv*/",
".vscode/",
"bin/",
"build/",
"configs/",
"customFiles/",
"docs/",
"exampleYmlFiles",
"gradle/",
"images/",
"logs/",
"pipeline/",
"scripts/",
"testings/",
".git-blame-ignore-revs",
".gitattributes",
".gitignore",
".pre-commit-config.yaml"
],
"java.signatureHelp.enabled": true,
"java.signatureHelp.description.enabled": true,
"java.maven.downloadSources": true,
"java.import.gradle.enabled": true,
"java.eclipse.downloadSources": true,
"java.import.gradle.wrapper.enabled": true,
"spring.initializr.defaultLanguage": "Java",
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
"spring.initializr.defaultArtifactId": "SPDF"
},
"extensions": [
"elagil.pre-commit-helper", // Support for pre-commit hooks to enforce code quality
"josevseb.google-java-format-for-vs-code", // Google Java code formatter to follow the Google Java Style Guide
"ms-python.black-formatter", // Python code formatter using Black
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
"vscjava.vscode-java-pack", // Java Extension Pack with essential Java tools for VS Code
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
"charliermarsh.ruff", // Ruff extension for Ruff language support
"github.vscode-github-actions" // GitHub Actions extension for Visual Studio Code
]
}
},
// Uncomment to connect as an existing user other than the container default. More info: https://aka.ms/dev-containers-non-root.
"remoteUser": "devuser",
"shutdownAction": "stopContainer",
"initializeCommand": "bash ./.devcontainer/git-init.sh",
"postStartCommand": "./.devcontainer/init-setup.sh"
}

View File

@ -1,19 +0,0 @@
#!/usr/bin/env bash
GIT_USER=$(git config --get user.name)
GIT_EMAIL=$(git config --get user.email)
# Exit if GIT_USER or GIT_EMAIL is empty
if [ -z "$GIT_USER" ] || [ -z "$GIT_EMAIL" ]; then
echo "GIT_USER or GIT_EMAIL is not set. Exiting."
exit 1
fi
git config --local user.name "$GIT_USER"
git config --local user.email "$GIT_EMAIL"
# This directory should contain custom Git hooks for the repository
# Set the path for Git hooks to /workspace/hooks
git config --local core.hooksPath '%(prefix)/workspace/hooks'
# Set the safe directory to the workspace path
git config --local --add safe.directory /workspace

View File

@ -1,75 +0,0 @@
#!/usr/bin/env bash
set -e
# =============================================================================
# Dev Container Initialization Script (init-setup.sh)
#
# This script runs when the Dev Container starts and provides guidance on
# how to interact with the project. It prints an ASCII logo, displays the
# current user, changes to the project root, and then shows helpful command
# instructions.
#
# Instructions for future developers:
#
# - To start the application, use:
# ./gradlew bootRun --no-daemon -Dspring-boot.run.fork=true -Dserver.address=0.0.0.0
#
# - To run tests, use:
# ./gradlew test
#
# - To build the project, use:
# ./gradlew build
#
# - For running pre-commit hooks (if configured), use:
# pre-commit run --all-files
#
# Make sure you are in the project root directory after this script executes.
# =============================================================================
echo "Devcontainer started successfully!"
VERSION=$(grep "^version =" build.gradle | awk -F'"' '{print $2}')
GRADLE_VERSION=$(gradle -version | grep "^Gradle " | awk '{print $2}')
GRADLE_PATH=$(which gradle)
JAVA_VERSION=$(java -version 2>&1 | awk -F '"' '/version/ {print $2}')
JAVA_PATH=$(which java)
echo """
____ _____ ___ ____ _ ___ _ _ ____ ____ ____ _____
/ ___|_ _|_ _| _ \| | |_ _| \ | |/ ___| | _ \| _ \| ___|
\___ \ | | | || |_) | | | || \| | | _ _____| |_) | | | | |_
___) || | | || _ <| |___ | || |\ | |_| |_____| __/| |_| | _|
|____/ |_| |___|_| \_\_____|___|_| \_|\____| |_| |____/|_|
"""
echo -e "Stirling-PDF Version: \e[32m$VERSION\e[0m"
echo -e "Gradle Version: \e[32m$GRADLE_VERSION\e[0m"
echo -e "Gradle Path: \e[32m$GRADLE_PATH\e[0m"
echo -e "Java Version: \e[32m$JAVA_VERSION\e[0m"
echo -e "Java Path: \e[32m$JAVA_PATH\e[0m"
# Display current active user (for permission/debugging purposes)
echo -e "Current user: \e[32m$(whoami)\e[0m"
# Change directory to the project root (parent directory of the script)
cd "$(dirname "$0")/.."
echo -e "Changed to project root: \e[32m$(pwd)\e[0m"
# Display available commands for developers
echo "=================================================================="
echo "Available commands:"
echo ""
echo " To start unoserver: "
echo -e "\e[34m nohup /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0 > /tmp/unoserver.log 2>&1 &\e[0m"
echo
echo " To start the application: "
echo -e "\e[34m gradle bootRun\e[0m"
echo ""
echo " To run tests: "
echo -e "\e[34m gradle test\e[0m"
echo ""
echo " To build the project: "
echo -e "\e[34m gradle build\e[0m"
echo ""
echo " To run pre-commit hooks (if configured):"
echo -e "\e[34m pre-commit run --all-files -c .pre-commit-config.yaml\e[0m"
echo "=================================================================="

View File

@ -1,31 +0,0 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
max_line_length = 127
insert_final_newline = true
trim_trailing_whitespace = true
[*.java]
indent_size = 4
max_line_length = 100
[*.py]
indent_size = 2
[*.gradle]
indent_size = 4
[*.html]
indent_size = 2
insert_final_newline = false
trim_trailing_whitespace = false
[*.js]
indent_size = 2
[*.yaml]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@ -54,8 +54,7 @@ Docker:
- any-glob-to-any-file: '.github/workflows/build.yml'
- any-glob-to-any-file: '.github/workflows/push-docker.yml'
- any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile.fat'
- any-glob-to-any-file: 'Dockerfile.ultra-lite'
- any-glob-to-any-file: 'Dockerfile.*'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml'
- any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: 'scripts/init.sh'
@ -64,16 +63,10 @@ Docker:
- any-glob-to-any-file: 'test.sh'
- any-glob-to-any-file: 'test2.sh'
Devtools:
- changed-files:
- any-glob-to-any-file: '.devcontainer/**/*'
- any-glob-to-any-file: 'Dockerfile.dev'
Test:
- changed-files:
- any-glob-to-any-file: 'cucumber/**/*'
- any-glob-to-any-file: 'src/test/**/*'
- any-glob-to-any-file: 'src/testing/**/*'
- any-glob-to-any-file: 'src/test**/*'
- any-glob-to-any-file: '.pre-commit-config'
- any-glob-to-any-file: '.github/workflows/pre_commit.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml'

3
.github/labels.yml vendored
View File

@ -108,6 +108,3 @@
- name: "Priority: Low"
color: "00FF00"
description: "Issues or pull requests with low priority"
- name: "Devtools"
color: "FF9E1F"
description: "Development tools"

4
.github/release.yml vendored
View File

@ -21,10 +21,6 @@ changelog:
labels:
- Translation
- title: Development Tools
labels:
- Devtools
- title: Other Changes
labels:
- "*"

View File

@ -164,7 +164,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
if current_entry["type"] == "entry":
if ref_entry_copy["type"] != "entry":
continue
if ref_entry_copy["key"].lower() == current_entry["key"].lower():
if ref_entry_copy["key"] == current_entry["key"]:
ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy)
write_json_file(os.path.join(branch, file_path), updated_properties)
@ -199,30 +199,29 @@ def check_for_differences(reference_file, file_list, branch, actor):
base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
for file_path in file_arr:
file_normpath = os.path.normpath(file_path)
absolute_path = os.path.abspath(file_normpath)
absolute_path = os.path.abspath(file_path)
# Verify that file is within the expected directory
if not absolute_path.startswith(base_dir):
raise ValueError(f"Unsafe file found: {file_normpath}")
raise ValueError(f"Unsafe file found: {file_path}")
# Verify file size before processing
if os.path.getsize(os.path.join(branch, file_normpath)) > MAX_FILE_SIZE:
if os.path.getsize(os.path.join(branch, file_path)) > MAX_FILE_SIZE:
raise ValueError(
f"The file {file_normpath} is too large and could pose a security risk."
f"The file {file_path} is too large and could pose a security risk."
)
basename_current_file = os.path.basename(os.path.join(branch, file_normpath))
basename_current_file = os.path.basename(os.path.join(branch, file_path))
if (
basename_current_file == basename_reference_file
or (
# only local windows command
not file_normpath.startswith(
not file_path.startswith(
os.path.join("", "src", "main", "resources", "messages_")
)
and not file_normpath.startswith(
and not file_path.startswith(
os.path.join(os.getcwd(), "src", "main", "resources", "messages_")
)
)
or not file_normpath.endswith(".properties")
or not file_path.endswith(".properties")
or not basename_current_file.startswith("messages_")
):
continue
@ -293,13 +292,13 @@ def check_for_differences(reference_file, file_list, branch, actor):
else:
report.append("2. **Test Status:** ✅ **_Passed_**")
if find_duplicate_keys(os.path.join(branch, file_normpath)):
if find_duplicate_keys(os.path.join(branch, file_path)):
has_differences = True
output = "\n".join(
[
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
for key, first, duplicate in find_duplicate_keys(
os.path.join(branch, file_normpath)
os.path.join(branch, file_path)
)
]
)

View File

@ -6,15 +6,13 @@ on:
permissions:
contents: read
issues: write # Required for adding reactions to comments
pull-requests: read # Required for reading PR information
jobs:
check-comment:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
issues: read
if: |
github.event.issue.pull_request &&
(
@ -36,23 +34,13 @@ jobs:
pr_number: ${{ steps.get-pr.outputs.pr_number }}
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
comment_id: ${{ github.event.comment.id }}
enable_security: ${{ steps.check-security-flag.outputs.enable_security }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
# Generate GitHub App token
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Get PR data
id: get-pr
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
@ -85,61 +73,19 @@ jobs:
core.setOutput('repository', repository);
core.setOutput('ref', pr.head.ref);
- name: Check for security/login flag
id: check-security-flag
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: |
if [[ "$COMMENT_BODY" == *"security"* ]] || [[ "$COMMENT_BODY" == *"login"* ]]; then
echo "Security flags detected in comment"
echo "enable_security=true" >> $GITHUB_OUTPUT
else
echo "No security flags detected in comment"
echo "enable_security=false" >> $GITHUB_OUTPUT
fi
- name: Add 'in_progress' reaction to comment
id: add-eyes-reaction
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
console.log(`Adding eyes reaction to comment ID: ${context.payload.comment.id}`);
try {
const { data: reaction } = await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
});
console.log(`Added reaction with ID: ${reaction.id}`);
return { success: true, id: reaction.id };
} catch (error) {
console.error(`Failed to add reaction: ${error.message}`);
console.error(error);
return { success: false, error: error.message };
}
deploy-pr:
needs: check-comment
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
- name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
@ -148,24 +94,19 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "temurin"
- name: Run Gradle Command
run: |
if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then
export DOCKER_ENABLE_SECURITY=true
else
export DOCKER_ENABLE_SECURITY=false
fi
./gradlew clean build
run: ./gradlew clean build
env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Get version number
id: versionNumber
@ -174,19 +115,19 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=alpha
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64
- name: Set up SSH
@ -196,21 +137,9 @@ jobs:
sudo chmod 600 ../private.key
- name: Deploy to VPS
id: deploy
run: |
# Set security settings based on flags
if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then
DOCKER_SECURITY="true"
LOGIN_SECURITY="true"
SECURITY_STATUS="🔒 Security Enabled"
else
DOCKER_SECURITY="false"
LOGIN_SECURITY="false"
SECURITY_STATUS="Security Disabled"
fi
# First create the docker-compose content locally
cat > docker-compose.yml << EOF
cat > docker-compose.yml << 'EOF'
version: '3.3'
services:
stirling-pdf:
@ -223,8 +152,8 @@ jobs:
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "${DOCKER_SECURITY}"
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
@ -238,7 +167,7 @@ jobs:
# Then copy the file and execute commands
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << ENDSSH
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
# Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
@ -251,65 +180,19 @@ jobs:
docker-compose up -d
ENDSSH
# Set output for use in PR comment
echo "security_status=${SECURITY_STATUS}" >> $GITHUB_ENV
- name: Add success reaction to comment
if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
console.log(`Adding rocket reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
try {
const { data: reaction } = await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ needs.check-comment.outputs.comment_id }},
content: 'rocket'
});
console.log(`Added rocket reaction with ID: ${reaction.id}`);
} catch (error) {
console.error(`Failed to add reaction: ${error.message}`);
console.error(error);
}
- name: Add failure reaction to comment
if: failure()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
console.log(`Adding -1 reaction to comment ID: ${{ needs.check-comment.outputs.comment_id }}`);
try {
const { data: reaction } = await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: ${{ needs.check-comment.outputs.comment_id }},
content: '-1'
});
console.log(`Added -1 reaction with ID: ${reaction.id}`);
} catch (error) {
console.error(`Failed to add reaction: ${error.message}`);
console.error(error);
}
- name: Post deployment URL to PR
if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
const securityStatus = process.env.security_status || "Security Disabled";
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
const commentBody = `## 🚀 PR Test Deployment\n\n` +
`Your PR has been deployed for testing!\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` +
`${securityStatus}\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
await github.rest.issues.createComment({

View File

@ -21,7 +21,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit

View File

@ -13,7 +13,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit

View File

@ -24,7 +24,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK ${{ matrix.jdk-version }}
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: ${{ matrix.jdk-version }}
distribution: "temurin"
@ -49,7 +49,7 @@ jobs:
- name: Upload Test Reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: test-reports-jdk-${{ matrix.jdk-version }}
path: |
@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -70,7 +70,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "adopt"
@ -80,7 +80,7 @@ jobs:
- name: FAILED - check the licenses for compatibility
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: dependencies-without-allowed-license.json
path: |
@ -106,7 +106,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -114,13 +114,13 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Java 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Install Docker Compose
run: |
@ -128,7 +128,7 @@ jobs:
sudo chmod +x /usr/local/bin/docker-compose
- name: Set up Python
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: "3.12"
cache: 'pip' # caching pip dependencies
@ -141,5 +141,4 @@ jobs:
run: |
chmod +x ./testing/test_webpages.sh
chmod +x ./testing/test.sh
chmod +x ./testing/test_disabledEndpoints.sh
./testing/test.sh

View File

@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -26,7 +26,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Python
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: "3.12"

View File

@ -17,11 +17,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: "Checkout Repository"
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: "Dependency Review"
uses: actions/dependency-review-action@ce3cf9537a52e8119d91fd484ab5b8a807627bf8 # v4.6.0
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4.5.0

View File

@ -18,13 +18,13 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -33,19 +33,19 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "adopt"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: check the licenses for compatibility
run: ./gradlew clean checkLicense
- name: FAILED - check the licenses for compatibility
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: dependencies-without-allowed-license.json
path: |
@ -69,7 +69,7 @@ jobs:
- name: Create Pull Request
id: cpr
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
with:
token: ${{ steps.generate-token.outputs.token }}
commit-message: "Update 3rd Party Licenses"

View File

@ -15,7 +15,7 @@ jobs:
issues: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -23,7 +23,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run Labeler
uses: crazy-max/ghaction-github-labeler@24d110aa46a59976b8a7f35518cb7f14f434c916 # v5.3.0
uses: crazy-max/ghaction-github-labeler@31674a3852a9074f2086abcf1c53839d466a47e7 # v5.2.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
yaml-file: .github/labels.yml

View File

@ -4,11 +4,6 @@ on:
workflow_dispatch:
release:
types: [created]
inputs:
test_mode:
description: "Run in test mode (skips release step)"
required: false
default: "false"
permissions:
contents: read
@ -21,7 +16,7 @@ jobs:
versionMac: ${{ steps.versionNumberMac.outputs.versionNumberMac }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -56,19 +51,19 @@ jobs:
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
with:
gradle-version: 8.12
@ -85,7 +80,7 @@ jobs:
mv ./build/libs/Stirling-PDF-${{ needs.read_versions.outputs.version }}.jar ./binaries/Stirling-PDF${{ matrix.file_suffix }}.jar
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
retention-days: 1
if-no-files-found: error
@ -106,12 +101,12 @@ jobs:
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: stirling-${{ matrix.file_suffix }}binaries
@ -119,7 +114,7 @@ jobs:
run: ls -R
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
retention-days: 1
if-no-files-found: error
@ -144,19 +139,19 @@ jobs:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 21
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "21"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
with:
gradle-version: 8.12
@ -175,35 +170,16 @@ jobs:
STIRLING_PDF_DESKTOP_UI: true
BROWSER_OPEN: true
- name: ☕ Set up JDK (x86_64)
if: matrix.os == 'macos-latest'
run: |
curl -L -o jdk.tar.gz https://cdn.azul.com/zulu/bin/zulu17.56.15-ca-jdk17.0.14-macosx_x64.tar.gz
mkdir -p zulu17
tar -xzf jdk.tar.gz -C zulu17 --strip-components=1
echo "JAVA_HOME=$PWD/zulu17" >> $GITHUB_ENV
echo "$PWD/zulu17/bin" >> $GITHUB_PATH
- name: Verify JDK architecture
if: matrix.os == 'macos-latest'
run: file $JAVA_HOME/bin/java
- name: Build project and run jpackage (x86_64)
if: matrix.os == 'macos-latest'
run: arch -x86_64 ./gradlew jpackageMacX64
# Rename and collect artifacts based on OS
- name: Prepare artifacts
id: prepare
shell: bash
run: |
ls -lah ./build/jpackage/
mkdir ./binaries
if [ "${{ matrix.os }}" = "windows-latest" ]; then
mv "./build/jpackage/Stirling-PDF-${{ needs.read_versions.outputs.version }}.exe" "./binaries/Stirling-PDF-win-installer.exe"
elif [ "${{ matrix.os }}" = "macos-latest" ]; then
mv "./build/jpackage/Stirling-PDF-${{ needs.read_versions.outputs.versionMac }}.dmg" "./binaries/Stirling-PDF-mac-installer.dmg"
mv "./build/jpackage/x86_64/Stirling-PDF (x86_64)-${{ needs.read_versions.outputs.versionMac }}.dmg" "./binaries/Stirling-PDF-mac-x86_64-installer.dmg"
else
mv "./build/jpackage/stirling-pdf_${{ needs.read_versions.outputs.version }}-1_amd64.deb" "./binaries/Stirling-PDF-linux-installer.deb"
fi
@ -212,7 +188,7 @@ jobs:
run: ls -R ./binaries
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
retention-days: 1
if-no-files-found: error
@ -234,12 +210,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: ${{ matrix.platform }}binaries
@ -279,30 +255,28 @@ jobs:
run: ls -R
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
retention-days: 1
if-no-files-found: error
name: ${{ matrix.platform }}signed
path: |
./Stirling-PDF-${{ matrix.platform }}installer.*
./Stirling-PDF-${{ matrix.platform }}x86_64-installer.*
!cosign.*
create-release:
if: github.event_name != 'workflow_dispatch' || github.event.inputs.test_mode != 'true'
needs: [read_versions, sign_verify, sign_verify-portable]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
- name: Display structure of downloaded files
run: ls -R
- name: Upload binaries, attestations and signatures to Release and create GitHub Release

View File

@ -16,13 +16,13 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -42,7 +42,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: 3.12
cache: 'pip' # caching pip dependencies
@ -61,7 +61,7 @@ jobs:
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
with:
token: ${{ steps.generate-token.outputs.token }}
commit-message: ":file_folder: pre-commit"

View File

@ -18,19 +18,19 @@ jobs:
id-token: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
with:
gradle-version: 8.12
@ -48,27 +48,27 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0
- name: Convert repository owner to lowercase
id: repoowner
@ -76,7 +76,7 @@ jobs:
- name: Generate tags
id: meta
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
@ -90,7 +90,7 @@ jobs:
- name: Build and push main Dockerfile
id: build-push-regular
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
@ -121,7 +121,7 @@ jobs:
- name: Generate tags ultra-lite
id: meta2
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
if: github.ref != 'refs/heads/main'
with:
images: |
@ -135,7 +135,7 @@ jobs:
- name: Build and push Dockerfile-ultra-lite
id: build-push-lite
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
if: github.ref != 'refs/heads/main'
with:
context: .
@ -152,7 +152,7 @@ jobs:
- name: Generate tags fat
id: meta3
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
if: github.ref != 'refs/heads/main'
with:
images: |
@ -166,7 +166,7 @@ jobs:
- name: Build and push main Dockerfile fat
id: build-push-fat
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
if: github.ref != 'refs/heads/main'
with:
builder: ${{ steps.buildx.outputs.name }}

View File

@ -23,19 +23,19 @@ jobs:
version: ${{ steps.versionNumber.outputs.versionNumber }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
with:
gradle-version: 8.12
@ -63,7 +63,7 @@ jobs:
ls -R ./build/launch4j
- name: Upload build artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: binaries${{ matrix.file_suffix }}
path: |
@ -83,12 +83,12 @@ jobs:
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Download build artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: binaries${{ matrix.file_suffix }}
- name: Display structure of downloaded files
@ -139,7 +139,7 @@ jobs:
./launch4j/Stirling-PDF-Server${{ matrix.file_suffix }}.exe
- name: Upload signed artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: signed${{ matrix.file_suffix }}
path: |
@ -161,12 +161,12 @@ jobs:
file_suffix: ""
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Download signed artifacts
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: signed${{ matrix.file_suffix }}

View File

@ -34,7 +34,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -66,7 +66,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: SARIF file
path: results.sarif
@ -74,6 +74,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10
with:
sarif_file: results.sarif

View File

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -27,7 +27,7 @@ jobs:
fetch-depth: 0
- name: Setup Gradle
uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: Build and analyze with Gradle
env:
@ -46,7 +46,7 @@ jobs:
- name: Upload Problems Report on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: gradle-problems-report
path: build/reports/problems/problems-report.html
@ -54,7 +54,7 @@ jobs:
- name: Upload Sonar Logs on Failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1
with:
name: sonar-logs
path: |

View File

@ -16,7 +16,7 @@ jobs:
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit

View File

@ -14,19 +14,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: "17"
distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1
- uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs
@ -35,7 +35,6 @@ jobs:
run: ./gradlew swaggerhubUpload
env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"
- name: Get version number
id: versionNumber
@ -43,7 +42,6 @@ jobs:
- name: Set API version as published and default on SwaggerHub
run: |
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/${SWAGGERHUB_USER}/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"

View File

@ -24,13 +24,13 @@ jobs:
committer: ${{ steps.committer.outputs.committer }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
with:
app-id: ${{ secrets.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -57,13 +57,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2.0.2
uses: actions/create-github-app-token@0d564482f06ca65fa9e77e2510873638c82206f2 # v1.11.5
with:
app-id: ${{ vars.GH_APP_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
@ -71,7 +71,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up Python
uses: actions/setup-python@8d9ed9ac5c53483de85588cdf95a591a75ab9f55 # v5.5.0
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: "3.12"
cache: 'pip' # caching pip dependencies
@ -103,7 +103,7 @@ jobs:
git diff --staged --quiet || git commit -m ":memo: Sync README.md" || echo "no changes"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
uses: peter-evans/create-pull-request@dd2324fc52d5d43c699a5636bcf19fceaa70c284 # v7.0.7
with:
token: ${{ steps.generate-token.outputs.token }}
commit-message: Update files

View File

@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -20,7 +20,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with:
java-version: '17'
distribution: 'temurin'
@ -31,7 +31,7 @@ jobs:
DOCKER_ENABLE_SECURITY: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Get version number
id: versionNumber
@ -40,13 +40,13 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push test image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
with:
context: .
file: ./Dockerfile
@ -105,7 +105,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit
@ -134,7 +134,7 @@ jobs:
steps:
- name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with:
egress-policy: audit

6
.gitignore vendored
View File

@ -26,8 +26,6 @@ clientWebUI/
!cucumber/exampleFiles/
!cucumber/exampleFiles/example_html.zip
exampleYmlFiles/stirling/
/testing/file_snapshots
SwaggerDoc.json
# Gradle
.gradle
@ -190,7 +188,3 @@ id_ed25519.pub
.ipynb_checkpoints
**/jcef-bundle/
# node_modules
node_modules/
*.mjs

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.6
rev: v0.8.4
hooks:
- id: ruff
args:
@ -12,7 +12,7 @@ repos:
files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$
exclude: (split_photos.py)
- repo: https://github.com/codespell-project/codespell
rev: v2.4.1
rev: v2.3.0
hooks:
- id: codespell
args:
@ -22,7 +22,7 @@ repos:
files: \.(html|css|js|py|md)$
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.3
rev: v8.22.0
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks

View File

@ -5,16 +5,19 @@
"ms-python.black-formatter", // Python code formatter using Black
"ms-python.flake8", // Flake8 linter for Python to enforce code quality
"ms-python.python", // Official Microsoft Python extension with IntelliSense, debugging, and Jupyter support
"ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development
// "ms-vscode-remote.remote-containers", // Support for remote development with containers (Docker, Dev Containers)
// "ms-vscode-remote.vscode-remote-extensionpack", // Remote Development Pack for SSH, WSL, and Containers
"Oracle.oracle-java", // Oracle Java extension with additional features for Java development
"redhat.java", // Java support by Red Hat with IntelliSense, debugging, and code navigation
"streetsidesoftware.code-spell-checker", // Spell checker for code to avoid typos
"vmware.vscode-boot-dev-pack", // Developer tools for Spring Boot by VMware
"vmware.vscode-spring-boot", // Spring Boot tools by VMware for enhanced Spring development
"vscjava.vscode-gradle", // Gradle extension for build and automation support
"vscjava.vscode-java-debug", // Debugging support for Java projects
"vscjava.vscode-java-dependency", // Java dependency management within VS Code
"vscjava.vscode-java-pack", // Java Extension Pack with essential Java tools for VS Code
"vscjava.vscode-java-test", // Java test framework for running and debugging tests in VS Code
"vscjava.vscode-spring-boot-dashboard", // Spring Boot dashboard for managing and visualizing Spring Boot applications
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
"charliermarsh.ruff" // Ruff code formatter for Python to follow the Ruff Style Guide
"vscjava.vscode-spring-initializr" // Support for Spring Initializr to create new Spring projects
]
}

79
.vscode/settings.json vendored
View File

@ -1,16 +1,80 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"files.eol": "auto",
"java.configuration.updateBuildConfiguration": "interactive",
"black-formatter.args": [
"--line-length",
"127"
],
"flake8.args": [
"--max-line-length",
"127"
],
"[java]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [
127
],
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
},
"[python]": {
"editor.tabSize": 2,
"editor.detectIndentation": false,
"editor.rulers": [
127
]
},
"[gradle-build]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [
127
]
},
"[gradle]": {
"editor.tabSize": 4,
"editor.detectIndentation": false,
"editor.rulers": [
127
]
},
"[html]": {
"editor.tabSize": 2,
"editor.rulers": [
127
],
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"[javascript]": {
"editor.tabSize": 2,
"editor.rulers": [
127
]
},
"[yaml]": {
"files.trimFinalNewlines": false,
"files.insertFinalNewline": false
},
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"files.autoSave": "onFocusChange",
"files.autoSaveWhenNoErrors": true,
"diffEditor.maxComputationTime": 0,
"editor.wordSegmenterLocales": "",
"editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active",
"cSpell.enabled": false,
"[java]": {
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
},
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "interactive",
"editor.indentSize": "tabSize",
"editor.stickyScroll.enabled": false,
"editor.minimap.enabled": false,
"editor.formatOnSave": true,
"editor.insertSpaces": true,
"java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle",
"java.format.settings.google.version": "1.26.0",
"java.format.settings.google.version": "1.25.2",
"java.format.settings.google.mode": "jar-file",
"java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
// (DE) Aktiviert Kommentare im Java-Format.
// (EN) Enables comments in Java formatting.
@ -80,4 +144,5 @@
"spring.initializr.defaultLanguage": "Java",
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
"spring.initializr.defaultArtifactId": "SPDF",
"cSpell.enabled": false,
}

View File

@ -124,7 +124,7 @@ These files provide pre-configured setups for different scenarios. For example,
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:

View File

@ -25,16 +25,20 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, conver
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
HOME=/home/stirlingpdfuser \
PUID=1000 \
PGID=1000 \
UMASK=022 \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
# JDK for app
@ -62,10 +66,6 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced features)
tesseract-ocr-data-eng \
tesseract-ocr-data-chi_sim \
tesseract-ocr-data-deu \
tesseract-ocr-data-fra \
tesseract-ocr-data-por \
# CV
py3-opencv \
python3 \
@ -73,8 +73,9 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
py3-pillow@testing \
py3-pdf2image@testing && \
python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
export PATH="/opt/venv/bin:$PATH" && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade unoserver weasyprint && \
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
@ -92,4 +93,4 @@ EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]

View File

@ -1,54 +0,0 @@
# dockerfile.dev
# Basisimage: Gradle mit JDK 17 (Debian-basiert)
FROM gradle:8.13-jdk17
# Als Root-Benutzer arbeiten, um benötigte Pakete zu installieren
USER root
# Set GRADLE_HOME und füge Gradle zum PATH hinzu
ENV GRADLE_HOME=/opt/gradle
ENV PATH="$GRADLE_HOME/bin:$PATH"
# Update und Installation zusätzlicher Pakete (Debian/Ubuntu-basiert)
RUN apt-get update && apt-get install -y \
sudo \
libreoffice \
poppler-utils \
qpdf \
# settings.yml | tessdataDir: /usr/share/tesseract-ocr/5/tessdata
tesseract-ocr \
tesseract-ocr-eng \
fonts-terminus fonts-dejavu fonts-font-awesome fonts-noto fonts-noto-core fonts-noto-cjk fonts-noto-extra fonts-liberation fonts-linuxlibertine \
python3-uno \
python3-venv \
# ss -tln
iproute2 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# Setze die Environment Variable für setuptools
ENV SETUPTOOLS_USE_DISTUTILS=local
# Installation der benötigten Python-Pakete
RUN python3 -m venv --system-site-packages /opt/venv \
&& . /opt/venv/bin/activate \
&& pip install --no-cache-dir WeasyPrint pdf2image pillow unoserver opencv-python-headless pre-commit
# Füge den venv-Pfad zur globalen PATH-Variable hinzu, damit die Tools verfügbar sind
ENV PATH="/opt/venv/bin:$PATH"
COPY . /workspace
RUN adduser --disabled-password --gecos '' devuser \
&& chown -R devuser:devuser /home/devuser /workspace
RUN echo "devuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/devuser \
&& chmod 0440 /etc/sudoers.d/devuser
# Setze das Arbeitsverzeichnis (wird später per Bind-Mount überschrieben)
WORKDIR /workspace
RUN chmod +x /workspace/.devcontainer/git-init.sh
RUN sudo chmod +x /workspace/.devcontainer/init-setup.sh
# Wechsel zum NichtRoot Benutzer
USER devuser

View File

@ -1,11 +1,5 @@
# Build the application
FROM gradle:8.13-jdk21 AS build
COPY build.gradle .
COPY settings.gradle .
COPY gradlew .
COPY gradle gradle/
RUN ./gradlew build -x spotlessApply -x spotlessCheck -x test -x sonarqube || return 0
FROM gradle:8.12-jdk17 AS build
# Set the working directory
WORKDIR /app
@ -16,7 +10,7 @@ COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false
RUN DOCKER_ENABLE_SECURITY=true \
STIRLING_PDF_DESKTOP_UI=false \
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
./gradlew clean build
# Main stage
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
@ -32,8 +26,13 @@ ARG VERSION_TAG
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
HOME=/home/stirlingpdfuser \
PUID=1000 \
PGID=1000 \
@ -42,8 +41,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \
PATH=$PATH:/opt/venv/bin
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
# JDK for app
@ -71,10 +69,7 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
# OCR MY PDF (unpaper for descew and other advanced featues)
qpdf \
tesseract-ocr-data-eng \
tesseract-ocr-data-chi_sim \
tesseract-ocr-data-deu \
tesseract-ocr-data-fra \
tesseract-ocr-data-por \
font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
# CV
py3-opencv \
@ -83,8 +78,9 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
py3-pillow@testing \
py3-pdf2image@testing && \
python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \
export PATH="/opt/venv/bin:$PATH" && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade unoserver weasyprint && \
ln -s /usr/lib/libreoffice/program/uno.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \
@ -101,4 +97,4 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 127.0.0.1"]
CMD ["sh", "-c", "java -Dfile.encoding=UTF-8 -jar /app.jar & /opt/venv/bin/unoserver --port 2003 --interface 0.0.0.0"]

View File

@ -7,8 +7,13 @@ ARG VERSION_TAG
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_BASE_OPTS="-XX:+UnlockExperimentalVMOptions -XX:MaxRAMPercentage=75 -XX:InitiatingHeapOccupancyPercent=20 -XX:+G1PeriodicGCInvokesConcurrent -XX:G1PeriodicGCInterval=10000 -XX:+UseStringDeduplication -XX:G1PeriodicGCSystemLoadThreshold=70" \
JAVA_CUSTOM_OPTS="" \
JAVA_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
-XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
PUID=1000 \
PGID=1000 \
UMASK=022

View File

@ -116,46 +116,46 @@ Stirling-PDF currently supports 39 languages!
| Language | Progress |
| -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![84%](https://geps.dev/progress/84) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![83%](https://geps.dev/progress/83) |
| Basque (Euskara) (eu_ES) | ![48%](https://geps.dev/progress/48) |
| Bulgarian (Български) (bg_BG) | ![93%](https://geps.dev/progress/93) |
| Catalan (Català) (ca_CA) | ![90%](https://geps.dev/progress/90) |
| Croatian (Hrvatski) (hr_HR) | ![81%](https://geps.dev/progress/81) |
| Czech (Česky) (cs_CZ) | ![92%](https://geps.dev/progress/92) |
| Danish (Dansk) (da_DK) | ![80%](https://geps.dev/progress/80) |
| Dutch (Nederlands) (nl_NL) | ![79%](https://geps.dev/progress/79) |
| Arabic (العربية) (ar_AR) | ![89%](https://geps.dev/progress/89) |
| Azerbaijani (Azərbaycan Dili) (az_AZ) | ![88%](https://geps.dev/progress/88) |
| Basque (Euskara) (eu_ES) | ![51%](https://geps.dev/progress/51) |
| Bulgarian (Български) (bg_BG) | ![99%](https://geps.dev/progress/99) |
| Catalan (Català) (ca_CA) | ![80%](https://geps.dev/progress/80) |
| Croatian (Hrvatski) (hr_HR) | ![86%](https://geps.dev/progress/86) |
| Czech (Česky) (cs_CZ) | ![97%](https://geps.dev/progress/97) |
| Danish (Dansk) (da_DK) | ![85%](https://geps.dev/progress/85) |
| Dutch (Nederlands) (nl_NL) | ![85%](https://geps.dev/progress/85) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![92%](https://geps.dev/progress/92) |
| German (Deutsch) (de_DE) | ![94%](https://geps.dev/progress/94) |
| Greek (Ελληνικά) (el_GR) | ![91%](https://geps.dev/progress/91) |
| Hindi (हिंदी) (hi_IN) | ![92%](https://geps.dev/progress/92) |
| Hungarian (Magyar) (hu_HU) | ![89%](https://geps.dev/progress/89) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![81%](https://geps.dev/progress/81) |
| Irish (Gaeilge) (ga_IE) | ![92%](https://geps.dev/progress/92) |
| French (Français) (fr_FR) | ![96%](https://geps.dev/progress/96) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) |
| Greek (Ελληνικά) (el_GR) | ![97%](https://geps.dev/progress/97) |
| Hindi (हिंदी) (hi_IN) | ![98%](https://geps.dev/progress/98) |
| Hungarian (Magyar) (hu_HU) | ![95%](https://geps.dev/progress/95) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![86%](https://geps.dev/progress/86) |
| Irish (Gaeilge) (ga_IE) | ![98%](https://geps.dev/progress/98) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) |
| Korean (한국어) (ko_KR) | ![92%](https://geps.dev/progress/92) |
| Norwegian (Norsk) (no_NB) | ![86%](https://geps.dev/progress/86) |
| Persian (فارسی) (fa_IR) | ![88%](https://geps.dev/progress/88) |
| Polish (Polski) (pl_PL) | ![96%](https://geps.dev/progress/96) |
| Portuguese (Português) (pt_PT) | ![91%](https://geps.dev/progress/91) |
| Japanese (日本語) (ja_JP) | ![92%](https://geps.dev/progress/92) |
| Korean (한국어) (ko_KR) | ![98%](https://geps.dev/progress/98) |
| Norwegian (Norsk) (no_NB) | ![78%](https://geps.dev/progress/78) |
| Persian (فارسی) (fa_IR) | ![94%](https://geps.dev/progress/94) |
| Polish (Polski) (pl_PL) | ![85%](https://geps.dev/progress/85) |
| Portuguese (Português) (pt_PT) | ![97%](https://geps.dev/progress/97) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![75%](https://geps.dev/progress/75) |
| Russian (Русский) (ru_RU) | ![94%](https://geps.dev/progress/94) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![60%](https://geps.dev/progress/60) |
| Simplified Chinese (简体中文) (zh_CN) | ![93%](https://geps.dev/progress/93) |
| Slovakian (Slovensky) (sk_SK) | ![69%](https://geps.dev/progress/69) |
| Slovenian (Slovenščina) (sl_SI) | ![95%](https://geps.dev/progress/95) |
| Spanish (Español) (es_ES) | ![93%](https://geps.dev/progress/93) |
| Swedish (Svenska) (sv_SE) | ![87%](https://geps.dev/progress/87) |
| Thai (ไทย) (th_TH) | ![80%](https://geps.dev/progress/80) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![89%](https://geps.dev/progress/89) |
| Romanian (Română) (ro_RO) | ![80%](https://geps.dev/progress/80) |
| Russian (Русский) (ru_RU) | ![97%](https://geps.dev/progress/97) |
| Serbian Latin alphabet (Srpski) (sr_LATN_RS) | ![63%](https://geps.dev/progress/63) |
| Simplified Chinese (简体中文) (zh_CN) | ![99%](https://geps.dev/progress/99) |
| Slovakian (Slovensky) (sk_SK) | ![74%](https://geps.dev/progress/74) |
| Slovenian (Slovenščina) (sl_SI) | ![96%](https://geps.dev/progress/96) |
| Spanish (Español) (es_ES) | ![98%](https://geps.dev/progress/98) |
| Swedish (Svenska) (sv_SE) | ![92%](https://geps.dev/progress/92) |
| Thai (ไทย) (th_TH) | ![85%](https://geps.dev/progress/85) |
| Tibetan (བོད་ཡིག་) (zh_BO) | ![94%](https://geps.dev/progress/94) |
| Traditional Chinese (繁體中文) (zh_TW) | ![99%](https://geps.dev/progress/99) |
| Turkish (Türkçe) (tr_TR) | ![98%](https://geps.dev/progress/98) |
| Ukrainian (Українська) (uk_UA) | ![97%](https://geps.dev/progress/97) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![74%](https://geps.dev/progress/74) |
| Turkish (Türkçe) (tr_TR) | ![82%](https://geps.dev/progress/82) |
| Ukrainian (Українська) (uk_UA) | ![72%](https://geps.dev/progress/72) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![79%](https://geps.dev/progress/79) |
## Stirling PDF Enterprise

View File

@ -1,35 +1,31 @@
plugins {
id "java"
id "org.springframework.boot" version "3.4.4"
id "org.springframework.boot" version "3.4.3"
id "io.spring.dependency-management" version "1.1.7"
id "org.springdoc.openapi-gradle-plugin" version "1.9.0"
id "org.springdoc.openapi-gradle-plugin" version "1.8.0"
id "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.6"
id "com.diffplug.spotless" version "7.0.3"
id "com.diffplug.spotless" version "7.0.2"
id "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.1"
id "org.sonarqube" version "6.1.0.5360"
id "org.sonarqube" version "6.0.1.5171"
}
import com.github.jk1.license.render.*
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Files
import java.time.Year
ext {
springBootVersion = "3.4.4"
springBootVersion = "3.4.3"
pdfboxVersion = "3.0.4"
imageioVersion = "3.12.0"
lombokVersion = "1.18.38"
lombokVersion = "1.18.36"
bouncycastleVersion = "1.80"
springSecuritySamlVersion = "6.4.4"
springSecuritySamlVersion = "6.4.3"
openSamlVersion = "4.3.2"
tempJrePath = null
}
group = "stirling.software"
version = "0.45.6"
version = "0.43.1"
java {
// 17 is lowest but we support and recommend 21
@ -102,12 +98,11 @@ openApi {
apiDocsUrl = "http://localhost:8080/v1/api-docs"
outputDir = file("$projectDir")
outputFileName = "SwaggerDoc.json"
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
}
//0.11.5 to 2024.11.5
static def getMacVersion(String version) {
def currentYear = Year.now().getValue()
def getMacVersion(String version) {
def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
}
@ -118,7 +113,6 @@ jpackage {
mainJar = "Stirling-PDF-${project.version}.jar"
appName = "Stirling-PDF"
appVersion = project.version
// appVersion = "2005.45.1"
vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor"
icon = "src/main/resources/static/favicon.ico"
@ -164,21 +158,23 @@ jpackage {
appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns"
type = "dmg"
macPackageIdentifier = "Stirling-PDF"
macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially
// // Add license and other documentation to DMG
// /*macDmgContent = [
// "README.md",
// "LICENSE",
// "CHANGELOG.md"
// ]*/
//
// // Enable Mac-specific entitlements
// //macEntitlements = "entitlements.plist" // You'll need to create this file
//installDir = "Applications"
// Add license and other documentation to DMG
/*macDmgContent = [
"README.md",
"LICENSE",
"CHANGELOG.md"
]*/
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
}
// Linux-specific configuration
@ -224,109 +220,6 @@ jpackage {
licenseFile = "LICENSE"
}
tasks.register('jpackageMacX64') {
group = 'distribution'
description = 'Packages app for MacOS x86_64'
println "Running jpackageMacX64 task"
if (OperatingSystem.current().isMacOsX()) {
println "MacOS detected. Downloading temp JRE."
dependsOn("downloadTempJre")
} else {
return
}
doLast {
def jrePath = project.ext.tempJrePath
if (!jrePath) {
throw new GradleException("JRE path not found.")
}
def outputStream = new ByteArrayOutputStream()
def errorStream = new ByteArrayOutputStream()
def result = exec {
commandLine 'jpackage',
'--type', 'dmg',
'--name', 'Stirling-PDF (x86_64)',
'--input', 'build/libs',
'--main-jar', "Stirling-PDF-${project.version}.jar",
'--main-class', 'org.springframework.boot.loader.launch.JarLauncher',
'--runtime-image', file(jrePath + "/zulu-17.jre/Contents/Home"),
'--dest', 'build/jpackage/x86_64',
'--icon', 'src/main/resources/static/favicon.icns',
'--app-version', getMacVersion(project.version.toString()),
'--mac-package-name', 'Stirling-PDF (x86_64)',
'--mac-package-identifier', 'Stirling-PDF (x86_64)',
'--mac-app-category', 'public.app-category.productivity'
standardOutput = outputStream
errorOutput = errorStream
ignoreExitValue = true
}
def stdout = outputStream.toString("UTF-8")
def stderr = errorStream.toString("UTF-8")
if (!stdout.isBlank()) {
println "jpackage stdout:\n$stdout"
}
if (result.exitValue != 0) {
throw new GradleException("jpackage failed with exit code ${result.exitValue}.\n\n$stderr")
}
}
}
//jpackage.finalizedBy(jpackageMacX64)
tasks.register('downloadTempJre') {
group = 'distribution'
description = 'Downloads and extracts a temporary JRE'
doLast {
try {
def jreUrl = 'https://cdn.azul.com/zulu/bin/zulu17.56.15-ca-jre17.0.14-macosx_x64.tar.gz'
def tmpDir = Files.createTempDirectory('zulu-jre').toFile()
def jreArchive = new File(tmpDir, 'jre.tar.gz')
def jreDir = new File(tmpDir, 'jre')
println "🔽 Downloading JRE to $jreArchive..."
jreArchive.withOutputStream { out ->
new URI(jreUrl).toURL().withInputStream { from -> out << from }
}
println "📦 Extracting JRE to $jreDir..."
jreDir.mkdirs()
providers.exec {
commandLine 'tar', '-xzf', jreArchive.absolutePath, '-C', jreDir.absolutePath, '--strip-components=1'
}.result.get()
println "✅ JRE ready at: $jreDir"
ext.tempJrePath = jreDir.absolutePath
project.ext.tempJrePath = jreDir.absolutePath
} catch (Exception e) {
println "Failed to download JRE. ${e.getLocalizedMessage()}"
cleanTempJre
}
}
}
tasks.register('cleanTempJre') {
dependsOn('jpackageMacX64')
group = 'distribution'
description = 'Deletes the temporary JRE'
doLast {
def path = project.ext.tempJrePath
if (path && new File("$path").exists()) {
println "Cleaning up temporary JRE: $path"
new File("$path").parentFile.deleteDir()
}
}
}
launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico"
@ -363,9 +256,9 @@ launch4j {
spotless {
java {
target project.fileTree('src').include('**/*.java')
target project.fileTree('src/main/java')
googleJavaFormat("1.26.0").aosp().reorderImports(false)
googleJavaFormat("1.25.2").aosp().reorderImports(false)
importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
toggleOffOn()
@ -391,22 +284,18 @@ sonar {
// }
tasks.wrapper {
gradleVersion = "8.12"
distributionType = Wrapper.DistributionType.ALL
}
//tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:deprecation"
//}
configurations.all {
// Remove all commons-logging dependencies so that only spring-jcl is used
exclude group: 'commons-logging', module: 'commons-logging'
// Exclude Tomcat
exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
}
dependencies {
//tmp for security bumps
implementation 'ch.qos.logback:logback-core:1.5.18'
implementation 'ch.qos.logback:logback-classic:1.5.18'
implementation 'ch.qos.logback:logback-core:1.5.16'
implementation 'ch.qos.logback:logback-classic:1.5.16'
// Exclude vulnerable BouncyCastle version used in tableau
@ -423,7 +312,7 @@ dependencies {
}
//security updates
implementation "org.springframework:spring-webmvc:6.2.5"
implementation "org.springframework:spring-webmvc:6.2.3"
implementation("io.github.pixee:java-security-toolkit:1.2.1")
@ -437,17 +326,13 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:3.4.2"
implementation "org.springframework:spring-jdbc:6.2.5"
implementation "org.springframework:spring-jdbc:6.2.3"
implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database
@ -491,18 +376,24 @@ dependencies {
// Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0"
implementation "commons-io:commons-io:2.19.0"
implementation "commons-io:commons-io:2.18.0"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ("com.opencsv:opencsv:5.10")
implementation ("com.opencsv:opencsv:5.10") {
exclude group: "commons-logging", module: "commons-logging"
}
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion")
implementation ("org.apache.pdfbox:pdfbox:$pdfboxVersion") {
exclude group: "commons-logging", module: "commons-logging"
}
implementation "org.apache.pdfbox:preflight:$pdfboxVersion"
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion")
implementation ("org.apache.pdfbox:xmpbox:$pdfboxVersion") {
exclude group: "commons-logging", module: "commons-logging"
}
// https://mvnrepository.com/artifact/technology.tabula/tabula
implementation ('technology.tabula:tabula:1.0.5') {
@ -516,7 +407,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.6"
implementation "io.micrometer:micrometer-core:1.14.4"
implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0"
@ -543,28 +434,18 @@ compileJava {
}
task writeVersion {
def propsFile = file("$projectDir/src/main/resources/version.properties")
def propsDir = propsFile.parentFile
doLast {
if (!propsDir.exists()) {
propsDir.mkdirs()
}
def propsFile = file("src/main/resources/version.properties")
def props = new Properties()
props.setProperty("version", version)
props.store(propsFile.newWriter(), null)
}
}
processResources.dependsOn(writeVersion)
swaggerhubUpload {
// dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs
api = "Stirling-PDF" // The name of your API on SwaggerHub
owner = "${System.getenv().getOrDefault('SWAGGERHUB_USER', 'Frooodle')}" // Your SwaggerHub username (or organization name)
owner = "Frooodle" // Your SwaggerHub username (or organization name)
version = project.version // The version of your API
inputFile = file("SwaggerDoc.json") // The path to your Swagger docs
inputFile = "./SwaggerDoc.json" // The path to your Swagger docs
token = "${System.getenv("SWAGGERHUB_API_KEY")}" // Your SwaggerHub API key, passed as an environment variable
oas = "3.0.0" // The version of the OpenAPI Specification you"re using
}
@ -575,12 +456,12 @@ jar {
attributes "Implementation-Title": "Stirling-PDF",
"Implementation-Version": project.version
}
}
tasks.named("test") {
useJUnitPlatform()
}
task printVersion {
doLast {
println project.version
@ -592,7 +473,3 @@ task printMacVersion {
println getMacVersion(project.version.toString())
}
}
tasks.named('generateOpenApiDocs') {
doNotTrackState("Tracking state is not supported for this task")
}

View File

@ -1,35 +0,0 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Fat-Disable-Endpoints
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- ./stirling/latest/data:/usr/share/tessdata:rw
- ./stirling/latest/config:/configs:rw
- ./stirling/latest/logs:/logs:rw
- ../testing/allEndpointsRemovedSettings.yml:/configs/settings.yml:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "false"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest-fat with all Endpoints Disabled
UI_APPNAMENAVBAR: Stirling-PDF Latest-fat
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat-Postgres
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat-postgres
image: stirlingtools/stirling-pdf:latest-fat-postgres
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite-Security
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-ultra-lite
image: stirlingtools/stirling-pdf:latest-ultra-lite
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-ultra-lite
image: stirlingtools/stirling-pdf:latest-ultra-lite
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest
image: stirlingtools/stirling-pdf:latest
deploy:
resources:
limits:

View File

@ -1,7 +1,7 @@
services:
stirling-pdf:
container_name: Stirling-PDF-Security-Fat-with-login
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat
image: stirlingtools/stirling-pdf:latest-fat
deploy:
resources:
limits:

View File

@ -2,13 +2,13 @@ echo "Running Stirling PDF with DOCKER_ENABLE_SECURITY=${DOCKER_ENABLE_SECURITY}
# Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
if [ ! -f app-security.jar ]; then
echo "Trying to download from: https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://files.stirlingpdf.com/v$VERSION_TAG/Stirling-PDF-with-login.jar
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/v$VERSION_TAG/Stirling-PDF-with-login.jar
# If the first download attempt failed, try without the 'v' prefix
# If the first download attempt failed, try with the 'v' prefix
if [ $? -ne 0 ]; then
echo "Trying to download from: https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://files.stirlingpdf.com/$VERSION_TAG/Stirling-PDF-with-login.jar
echo "Trying to download from: https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar"
curl -L -o app-security.jar https://github.com/Stirling-Tools/Stirling-PDF/releases/download/$VERSION_TAG/Stirling-PDF-with-login.jar
fi
if [ $? -eq 0 ]; then # checks if curl was successful

View File

@ -1,8 +1,5 @@
#!/bin/bash
export JAVA_TOOL_OPTIONS="${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}"
echo "running with JAVA_TOOL_OPTIONS ${JAVA_BASE_OPTS} ${JAVA_CUSTOM_OPTS}"
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true

View File

@ -1,39 +0,0 @@
package org.apache.pdfbox.examples.util;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.pdfbox.io.RandomAccessReadBufferedFile;
import lombok.extern.slf4j.Slf4j;
/** A custom RandomAccessRead implementation that deletes the file when closed */
@Slf4j
public class DeletingRandomAccessFile extends RandomAccessReadBufferedFile {
private final Path tempFilePath;
public DeletingRandomAccessFile(File file) throws IOException {
super(file);
this.tempFilePath = file.toPath();
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
try {
boolean deleted = Files.deleteIfExists(tempFilePath);
if (deleted) {
log.info("Successfully deleted temp file: {}", tempFilePath);
} else {
log.warn("Failed to delete temp file (may not exist): {}", tempFilePath);
}
} catch (IOException e) {
log.error("Error deleting temp file: {}", tempFilePath, e);
}
}
}
}

View File

@ -7,11 +7,7 @@ import org.springframework.core.annotation.Order;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.EnterpriseEdition;
import stirling.software.SPDF.model.ApplicationProperties.Premium;
import stirling.software.SPDF.model.ApplicationProperties.Premium.ProFeatures.GoogleDrive;
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@ -26,100 +22,15 @@ public class EEAppConfig {
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker;
migrateEnterpriseSettingsToPremium(this.applicationProperties);
}
@Bean(name = "runningProOrHigher")
public boolean runningProOrHigher() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
}
@Bean(name = "runningEE")
public boolean runningEnterprise() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE;
public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getEnterpriseEnabledResult();
}
@Bean(name = "SSOAutoLogin")
public boolean ssoAutoLogin() {
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin();
}
@Bean(name = "GoogleDriveEnabled")
public boolean googleDriveEnabled() {
return runningProOrHigher()
&& applicationProperties.getPremium().getProFeatures().getGoogleDrive().isEnabled();
}
@Bean(name = "GoogleDriveConfig")
public GoogleDrive googleDriveConfig() {
return applicationProperties.getPremium().getProFeatures().getGoogleDrive();
}
// TODO: Remove post migration
public void migrateEnterpriseSettingsToPremium(ApplicationProperties applicationProperties) {
EnterpriseEdition enterpriseEdition = applicationProperties.getEnterpriseEdition();
Premium premium = applicationProperties.getPremium();
// Only proceed if both objects exist
if (enterpriseEdition == null || premium == null) {
return;
}
// Copy the license key if it's set in enterprise but not in premium
if (premium.getKey() == null
|| premium.getKey().equals("00000000-0000-0000-0000-000000000000")) {
if (enterpriseEdition.getKey() != null
&& !enterpriseEdition.getKey().equals("00000000-0000-0000-0000-000000000000")) {
premium.setKey(enterpriseEdition.getKey());
}
}
// Copy enabled state if enterprise is enabled but premium is not
if (!premium.isEnabled() && enterpriseEdition.isEnabled()) {
premium.setEnabled(true);
}
// Copy SSO auto login setting
if (!premium.getProFeatures().isSsoAutoLogin() && enterpriseEdition.isSsoAutoLogin()) {
premium.getProFeatures().setSsoAutoLogin(true);
}
// Copy CustomMetadata settings
Premium.ProFeatures.CustomMetadata premiumMetadata =
premium.getProFeatures().getCustomMetadata();
EnterpriseEdition.CustomMetadata enterpriseMetadata = enterpriseEdition.getCustomMetadata();
if (enterpriseMetadata != null && premiumMetadata != null) {
// Copy autoUpdateMetadata setting
if (!premiumMetadata.isAutoUpdateMetadata()
&& enterpriseMetadata.isAutoUpdateMetadata()) {
premiumMetadata.setAutoUpdateMetadata(true);
}
// Copy author if not set in premium but set in enterprise
if ((premiumMetadata.getAuthor() == null
|| premiumMetadata.getAuthor().trim().isEmpty()
|| "username".equals(premiumMetadata.getAuthor()))
&& enterpriseMetadata.getAuthor() != null
&& !enterpriseMetadata.getAuthor().trim().isEmpty()) {
premiumMetadata.setAuthor(enterpriseMetadata.getAuthor());
}
// Copy creator if not set in premium but set in enterprise and different from default
if ((premiumMetadata.getCreator() == null
|| "Stirling-PDF".equals(premiumMetadata.getCreator()))
&& enterpriseMetadata.getCreator() != null
&& !"Stirling-PDF".equals(enterpriseMetadata.getCreator())) {
premiumMetadata.setCreator(enterpriseMetadata.getCreator());
}
// Copy producer if not set in premium but set in enterprise and different from default
if ((premiumMetadata.getProducer() == null
|| "Stirling-PDF".equals(premiumMetadata.getProducer()))
&& enterpriseMetadata.getProducer() != null
&& !"Stirling-PDF".equals(enterpriseMetadata.getProducer())) {
premiumMetadata.setProducer(enterpriseMetadata.getProducer());
}
}
return applicationProperties.getEnterpriseEdition().isSsoAutoLogin();
}
}

View File

@ -4,17 +4,12 @@ import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Base64;
import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.signers.Ed25519Signer;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONException;
import com.posthog.java.shaded.org.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
@ -25,26 +20,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Service
@Slf4j
public class KeygenLicenseVerifier {
enum License {
NORMAL,
PRO,
ENTERPRISE
}
// License verification configuration
// todo: place in config files?
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372";
private static final String BASE_URL = "https://api.keygen.sh/v1/accounts";
private static final String PUBLIC_KEY =
"9fbc0d78593dcfcf03c945146edd60083bf5fae77dbc08aaa3935f03ce94a58d";
private static final String CERT_PREFIX = "-----BEGIN LICENSE FILE-----";
private static final String CERT_SUFFIX = "-----END LICENSE FILE-----";
private static final String JWT_PREFIX = "key/";
private static final ObjectMapper objectMapper = new ObjectMapper();
private final ApplicationProperties applicationProperties;
@Autowired
@ -52,378 +32,9 @@ public class KeygenLicenseVerifier {
this.applicationProperties = applicationProperties;
}
public License verifyLicense(String licenseKeyOrCert) {
License license;
if (isCertificateLicense(licenseKeyOrCert)) {
log.info("Detected certificate-based license. Processing...");
boolean isValid = verifyCertificateLicense(licenseKeyOrCert);
if (isValid) {
license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
} else {
license = License.NORMAL;
}
} else if (isJWTLicense(licenseKeyOrCert)) {
log.info("Detected JWT-style license key. Processing...");
boolean isValid = verifyJWTLicense(licenseKeyOrCert);
if (isValid) {
license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
} else {
license = License.NORMAL;
}
} else {
log.info("Detected standard license key. Processing...");
boolean isValid = verifyStandardLicense(licenseKeyOrCert);
if (isValid) {
license = isEnterpriseLicense ? License.ENTERPRISE : License.PRO;
} else {
license = License.NORMAL;
}
}
return license;
}
private boolean isEnterpriseLicense = false;
private boolean isCertificateLicense(String license) {
return license != null && license.trim().startsWith(CERT_PREFIX);
}
private boolean isJWTLicense(String license) {
return license != null && license.trim().startsWith(JWT_PREFIX);
}
private boolean verifyCertificateLicense(String licenseFile) {
public boolean verifyLicense(String licenseKey) {
try {
String encodedPayload = licenseFile;
// Remove the header
encodedPayload = encodedPayload.replace(CERT_PREFIX, "");
// Remove the footer
encodedPayload = encodedPayload.replace(CERT_SUFFIX, "");
// Remove all newlines
encodedPayload = encodedPayload.replaceAll("\\r?\\n", "");
byte[] payloadBytes = Base64.getDecoder().decode(encodedPayload);
String payload = new String(payloadBytes);
log.info("Decoded certificate payload: {}", payload);
String encryptedData = "";
String encodedSignature = "";
String algorithm = "";
try {
JSONObject attrs = new JSONObject(payload);
encryptedData = (String) attrs.get("enc");
encodedSignature = (String) attrs.get("sig");
algorithm = (String) attrs.get("alg");
} catch (JSONException e) {
log.error("Failed to parse license file: {}", e.getMessage());
return false;
}
// Verify license file algorithm
if (!algorithm.equals("base64+ed25519")) {
log.error(
"Unsupported algorithm: {}. Only base64+ed25519 is supported.", algorithm);
return false;
}
// Verify signature
boolean isSignatureValid = verifyEd25519Signature(encryptedData, encodedSignature);
if (!isSignatureValid) {
log.error("License file signature is invalid");
return false;
}
log.info("License file signature is valid");
// Decode the base64 data
String decodedData;
try {
decodedData = new String(Base64.getDecoder().decode(encryptedData));
} catch (IllegalArgumentException e) {
log.error("Failed to decode license data: {}", e.getMessage());
return false;
}
// Process the certificate data
boolean isValid = processCertificateData(decodedData);
return isValid;
} catch (Exception e) {
log.error("Error verifying certificate license: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyEd25519Signature(String encryptedData, String encodedSignature) {
try {
log.info("Signature to verify: {}", encodedSignature);
byte[] signatureBytes = Base64.getDecoder().decode(encodedSignature);
// Create the signing data format - prefix with "license/"
String signingData = String.format("license/%s", encryptedData);
byte[] signingDataBytes = signingData.getBytes();
log.info("Signing data length: {} bytes", signingDataBytes.length);
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
Ed25519PublicKeyParameters verifierParams =
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, verifierParams);
verifier.update(signingDataBytes, 0, signingDataBytes.length);
// Verify the signature
boolean result = verifier.verifySignature(signatureBytes);
if (!result) {
log.error("Signature verification failed with standard public key");
}
return result;
} catch (Exception e) {
log.error("Error verifying Ed25519 signature: {}", e.getMessage(), e);
return false;
}
}
private boolean processCertificateData(String certData) {
try {
JSONObject licenseData = new JSONObject(certData);
JSONObject metaObj = licenseData.optJSONObject("meta");
if (metaObj != null) {
String issuedStr = metaObj.optString("issued", null);
String expiryStr = metaObj.optString("expiry", null);
if (issuedStr != null && expiryStr != null) {
java.time.Instant issued = java.time.Instant.parse(issuedStr);
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
if (issued.isAfter(now)) {
log.error(
"License file issued date is in the future. Please adjust system time or request a new license");
return false;
}
// Check if the license file has expired
if (expiry.isBefore(now)) {
log.error("License file has expired on {}", expiryStr);
return false;
}
log.info("License file valid until {}", expiryStr);
}
}
// Get the main license data
JSONObject dataObj = licenseData.optJSONObject("data");
if (dataObj == null) {
log.error("No data object found in certificate");
return false;
}
// Extract license or machine information
JSONObject attributesObj = dataObj.optJSONObject("attributes");
if (attributesObj != null) {
log.info("Found attributes in certificate data");
// Extract metadata
JSONObject metadataObj = attributesObj.optJSONObject("metadata");
if (metadataObj != null) {
int users = metadataObj.optInt("users", 0);
if (users > 0) {
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
}
isEnterpriseLicense = metadataObj.optBoolean("isEnterprise", false);
}
// Check license status if available
String status = attributesObj.optString("status", null);
if (status != null
&& !status.equals("ACTIVE")
&& !status.equals("EXPIRING")) { // Accept "EXPIRING" status as valid
log.error("License status is not active: {}", status);
return false;
}
}
return true;
} catch (Exception e) {
log.error("Error processing certificate data: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyJWTLicense(String licenseKey) {
try {
log.info("Verifying ED25519_SIGN format license key");
// Remove the "key/" prefix
String licenseData = licenseKey.substring(JWT_PREFIX.length());
// Split into payload and signature
String[] parts = licenseData.split("\\.", 2);
if (parts.length != 2) {
log.error(
"Invalid ED25519_SIGN license format. Expected format: key/payload.signature");
return false;
}
String encodedPayload = parts[0];
String encodedSignature = parts[1];
// Verify signature
boolean isSignatureValid = verifyJWTSignature(encodedPayload, encodedSignature);
if (!isSignatureValid) {
log.error("ED25519_SIGN license signature is invalid");
return false;
}
log.info("ED25519_SIGN license signature is valid");
// Decode and process payload - first convert from URL-safe base64 if needed
String base64Payload = encodedPayload.replace('-', '+').replace('_', '/');
byte[] payloadBytes = Base64.getDecoder().decode(base64Payload);
String payload = new String(payloadBytes);
// Process the license payload
boolean isValid = processJWTLicensePayload(payload);
return isValid;
} catch (Exception e) {
log.error("Error verifying ED25519_SIGN license: {}", e.getMessage());
return false;
}
}
private boolean verifyJWTSignature(String encodedPayload, String encodedSignature) {
try {
// Decode base64 signature
byte[] signatureBytes =
Base64.getDecoder()
.decode(encodedSignature.replace('-', '+').replace('_', '/'));
// For ED25519_SIGN format, the signing data is "key/" + encodedPayload
String signingData = String.format("key/%s", encodedPayload);
byte[] dataBytes = signingData.getBytes();
byte[] publicKeyBytes = Hex.decode(PUBLIC_KEY);
Ed25519PublicKeyParameters verifierParams =
new Ed25519PublicKeyParameters(publicKeyBytes, 0);
Ed25519Signer verifier = new Ed25519Signer();
verifier.init(false, verifierParams);
verifier.update(dataBytes, 0, dataBytes.length);
// Verify the signature
return verifier.verifySignature(signatureBytes);
} catch (Exception e) {
log.error("Error verifying JWT signature: {}", e.getMessage());
return false;
}
}
private boolean processJWTLicensePayload(String payload) {
try {
log.info("Processing license payload: {}", payload);
JSONObject licenseData = new JSONObject(payload);
JSONObject licenseObj = licenseData.optJSONObject("license");
if (licenseObj == null) {
String id = licenseData.optString("id", null);
if (id != null) {
log.info("Found license ID: {}", id);
licenseObj = licenseData; // Use the root object as the license object
} else {
log.error("License data not found in payload");
return false;
}
}
String licenseId = licenseObj.optString("id", "unknown");
log.info("Processing license with ID: {}", licenseId);
// Check expiry date
String expiryStr = licenseObj.optString("expiry", null);
if (expiryStr != null && !expiryStr.equals("null")) {
java.time.Instant expiry = java.time.Instant.parse(expiryStr);
java.time.Instant now = java.time.Instant.now();
if (now.isAfter(expiry)) {
log.error("License has expired on {}", expiryStr);
return false;
}
log.info("License valid until {}", expiryStr);
} else {
log.info("License has no expiration date");
}
// Extract account, product, policy info
JSONObject accountObj = licenseData.optJSONObject("account");
if (accountObj != null) {
String accountId = accountObj.optString("id", "unknown");
log.info("License belongs to account: {}", accountId);
// Verify this matches your expected account ID
if (!ACCOUNT_ID.equals(accountId)) {
log.warn("License account ID does not match expected account ID");
// You might want to fail verification here depending on your requirements
}
}
// Extract policy information if available
JSONObject policyObj = licenseData.optJSONObject("policy");
if (policyObj != null) {
String policyId = policyObj.optString("id", "unknown");
log.info("License uses policy: {}", policyId);
// Extract max users and isEnterprise from policy or metadata
int users = policyObj.optInt("users", 0);
isEnterpriseLicense = policyObj.optBoolean("isEnterprise", false);
if (users > 0) {
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users", users);
} else {
// Try to get users from metadata if present
Object metadataObj = policyObj.opt("metadata");
if (metadataObj instanceof JSONObject) {
JSONObject metadata = (JSONObject) metadataObj;
users = metadata.optInt("users", 1);
applicationProperties.getPremium().setMaxUsers(users);
log.info("License allows for {} users (from metadata)", users);
// Check for isEnterprise flag in metadata
isEnterpriseLicense = metadata.optBoolean("isEnterprise", false);
} else {
// Default value
applicationProperties.getPremium().setMaxUsers(1);
log.info("Using default of 1 user for license");
}
}
}
return true;
} catch (Exception e) {
log.error("Error processing license payload: {}", e.getMessage(), e);
return false;
}
}
private boolean verifyStandardLicense(String licenseKey) {
try {
log.info("Checking standard license key");
log.info("Checking license key");
String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license
@ -433,7 +44,7 @@ public class KeygenLicenseVerifier {
String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) {
String code = validationResponse.path("meta").path("code").asText();
log.info(code);
log.debug(code);
if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code)
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
@ -458,7 +69,7 @@ public class KeygenLicenseVerifier {
return false;
} catch (Exception e) {
log.error("Error verifying standard license: {}", e.getMessage());
log.error("Error verifying license: {}", e.getMessage());
return false;
}
}
@ -485,7 +96,7 @@ public class KeygenLicenseVerifier {
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.info("ValidateLicenseResponse body: {}", response.body());
log.debug("ValidateLicenseResponse body: {}", response.body());
JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta");
@ -494,11 +105,10 @@ public class KeygenLicenseVerifier {
String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText();
log.info("License validity: " + isValid);
log.info("Validation detail: " + detail);
log.info("Validation code: " + code);
log.debug("License validity: " + isValid);
log.debug("Validation detail: " + detail);
log.debug("Validation code: " + code);
// Extract user count
int users =
jsonResponse
.path("data")
@ -506,17 +116,7 @@ public class KeygenLicenseVerifier {
.path("metadata")
.path("users")
.asInt(0);
applicationProperties.getPremium().setMaxUsers(users);
// Extract isEnterprise flag
isEnterpriseLicense =
jsonResponse
.path("data")
.path("attributes")
.path("metadata")
.path("isEnterprise")
.asBoolean(false);
applicationProperties.getEnterpriseEdition().setMaxUsers(users);
log.info(applicationProperties.toString());
} else {
@ -548,8 +148,13 @@ public class KeygenLicenseVerifier {
.put("fingerprint", machineFingerprint)
.put(
"platform",
System.getProperty("os.name"))
.put("name", hostname))
System.getProperty(
"os.name")) // Added
// platform
// parameter
.put(
"name",
hostname)) // Added name parameter
.put(
"relationships",
new JSONObject()
@ -571,12 +176,16 @@ public class KeygenLicenseVerifier {
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json")
.header("Authorization", "License " + licenseKey)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.header(
"Authorization",
"License " + licenseKey) // Keep the license key authentication
.POST(
HttpRequest.BodyPublishers.ofString(
body.toString())) // Send the JSON body
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
log.info("activateMachine Response body: " + response.body());
log.debug("activateMachine Response body: " + response.body());
if (response.statusCode() == 201) {
log.info("Machine activated successfully");
return true;

View File

@ -1,9 +1,6 @@
package stirling.software.SPDF.EE;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
@ -11,7 +8,6 @@ import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@ -19,13 +15,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Slf4j
public class LicenseKeyChecker {
private static final String FILE_PREFIX = "file:";
private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties;
private License premiumEnabledResult = License.NORMAL;
private boolean enterpriseEnabledResult = false;
@Autowired
public LicenseKeyChecker(
@ -41,60 +35,27 @@ public class LicenseKeyChecker {
}
private void checkLicense() {
if (!applicationProperties.getPremium().isEnabled()) {
premiumEnabledResult = License.NORMAL;
if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
enterpriseEnabledResult = false;
} else {
String licenseKey = getLicenseKeyContent(applicationProperties.getPremium().getKey());
if (licenseKey != null) {
premiumEnabledResult = licenseService.verifyLicense(licenseKey);
if (License.ENTERPRISE == premiumEnabledResult) {
log.info("License key is Enterprise.");
} else if (License.PRO == premiumEnabledResult) {
log.info("License key is Pro.");
enterpriseEnabledResult =
licenseService.verifyLicense(
applicationProperties.getEnterpriseEdition().getKey());
if (enterpriseEnabledResult) {
log.info("License key is valid.");
} else {
log.info("License key is invalid, defaulting to non pro license.");
}
} else {
log.error("Failed to obtain license key content.");
premiumEnabledResult = License.NORMAL;
log.info("License key is invalid.");
}
}
}
private String getLicenseKeyContent(String keyOrFilePath) {
if (keyOrFilePath == null || keyOrFilePath.trim().isEmpty()) {
log.error("License key is not specified");
return null;
}
// Check if it's a file reference
if (keyOrFilePath.startsWith(FILE_PREFIX)) {
String filePath = keyOrFilePath.substring(FILE_PREFIX.length());
try {
Path path = Paths.get(filePath);
if (!Files.exists(path)) {
log.error("License file does not exist: {}", filePath);
return null;
}
log.info("Reading license from file: {}", filePath);
return Files.readString(path);
} catch (IOException e) {
log.error("Failed to read license file: {}", e.getMessage());
return null;
}
}
// It's a direct license key
return keyOrFilePath;
}
public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getPremium().setKey(newKey);
applicationProperties.getEnterpriseEdition().setKey(newKey);
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
checkLicense();
}
public License getPremiumLicenseEnabledResult() {
return premiumEnabledResult;
public boolean getEnterpriseEnabledResult() {
return enterpriseEnabledResult;
}
}

View File

@ -37,7 +37,6 @@ public class SPDFApplication {
private static String serverPortStatic;
private static String baseUrlStatic;
private static String contextPathStatic;
private final Environment env;
private final ApplicationProperties applicationProperties;
@ -46,9 +45,6 @@ public class SPDFApplication {
@Value("${baseUrl:http://localhost}")
private String baseUrl;
@Value("${server.servlet.context-path:/}")
private String contextPath;
public SPDFApplication(
Environment env,
ApplicationProperties applicationProperties,
@ -142,8 +138,7 @@ public class SPDFApplication {
@PostConstruct
public void init() {
baseUrlStatic = this.baseUrl;
contextPathStatic = this.contextPath;
String url = baseUrl + ":" + getStaticPort() + contextPath;
String url = baseUrl + ":" + getStaticPort();
if (webBrowser != null
&& Boolean.parseBoolean(System.getProperty("STIRLING_PDF_DESKTOP_UI", "false"))) {
webBrowser.initWebUI(url);
@ -200,7 +195,7 @@ public class SPDFApplication {
private static void printStartupLogs() {
log.info("Stirling-PDF Started.");
String url = baseUrlStatic + ":" + getStaticPort() + contextPathStatic;
String url = baseUrlStatic + ":" + getStaticPort();
log.info("Navigate to {}", url);
}
@ -225,8 +220,4 @@ public class SPDFApplication {
public static String getStaticPort() {
return serverPortStatic;
}
public static String getStaticContextPath() {
return contextPathStatic;
}
}

View File

@ -93,21 +93,8 @@ public class DesktopBrowser implements WebBrowser {
setupMainFrame();
setupLoadHandler();
// Force initialize UI after 7 seconds if not already done
Timer timeoutTimer =
new Timer(
2500,
e -> {
log.warn(
"Loading timeout reached. Forcing"
+ " UI transition.");
if (!browserInitialized) {
// Force UI initialization
forceInitializeUI();
}
});
timeoutTimer.setRepeats(false);
timeoutTimer.start();
// Show the frame immediately but transparent
frame.setVisible(true);
});
} catch (Exception e) {
log.error("Error initializing JCEF browser: ", e);
@ -251,8 +238,8 @@ public class DesktopBrowser implements WebBrowser {
boolean canGoBack,
boolean canGoForward) {
log.debug(
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:"
+ " {}, browserInitialized: {}, Time elapsed: {}ms",
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
+ "browserInitialized: {}, Time elapsed: {}ms",
isLoading,
canGoBack,
canGoForward,
@ -261,8 +248,7 @@ public class DesktopBrowser implements WebBrowser {
if (!isLoading && !browserInitialized) {
log.info(
"Browser finished loading, preparing to initialize UI"
+ " components");
"Browser finished loading, preparing to initialize UI components");
browserInitialized = true;
SwingUtilities.invokeLater(
() -> {
@ -303,12 +289,10 @@ public class DesktopBrowser implements WebBrowser {
browser.getUIComponent()
.requestFocus();
log.info(
"Browser component"
+ " focused");
"Browser component focused");
} catch (Exception ex) {
log.error(
"Error focusing"
+ " browser",
"Error focusing browser",
ex);
}
});
@ -431,67 +415,4 @@ public class DesktopBrowser implements WebBrowser {
if (cefApp != null) cefApp.dispose();
if (loadingWindow != null) loadingWindow.dispose();
}
public static void forceInitializeUI() {
try {
if (loadingWindow != null) {
log.info("Forcing start of UI initialization sequence");
// Close loading window first
loadingWindow.setVisible(false);
loadingWindow.dispose();
loadingWindow = null;
log.info("Loading window disposed");
// Then setup the main frame
frame.setVisible(false);
frame.dispose();
frame.setOpacity(1.0f);
frame.setUndecorated(false);
frame.pack();
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
frame.setLocationRelativeTo(null);
log.debug("Frame reconfigured");
// Show the main frame
frame.setVisible(true);
frame.requestFocus();
frame.toFront();
log.info("Main frame displayed and focused");
// Focus the browser component if available
if (browser != null) {
Timer focusTimer =
new Timer(
100,
e -> {
try {
browser.getUIComponent().requestFocus();
log.info("Browser component focused");
} catch (Exception ex) {
log.error(
"Error focusing browser during force ui"
+ " initialization.",
ex);
}
});
focusTimer.setRepeats(false);
focusTimer.start();
}
}
} catch (Exception e) {
log.error("Error during Forced UI initialization.", e);
// Attempt cleanup on error
if (loadingWindow != null) {
loadingWindow.dispose();
loadingWindow = null;
}
if (frame != null) {
frame.setVisible(true);
frame.setOpacity(1.0f);
frame.setUndecorated(false);
frame.requestFocus();
}
}
}
}

View File

@ -8,7 +8,6 @@ import java.util.List;
import java.util.Properties;
import java.util.function.Predicate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@ -20,8 +19,6 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine;
import com.posthog.java.shaded.kotlin.text.Regex;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties;
@ -81,11 +78,6 @@ public class AppConfig {
return applicationProperties.getUi().getLanguages();
}
@Bean
public String contextPath(@Value("${server.servlet.context-path}") String contextPath) {
return contextPath;
}
@Bean(name = "navBarText")
public String navBarText() {
String defaultNavBar =
@ -184,7 +176,7 @@ public class AppConfig {
@Bean(name = "analyticsEnabled")
@Scope("request")
public boolean analyticsEnabled() {
if (applicationProperties.getPremium().isEnabled()) return true;
if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().isAnalyticsEnabled();
}

View File

@ -9,7 +9,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@ -57,8 +56,6 @@ public class ConfigInitializer {
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
YamlHelper settingsFile = new YamlHelper(settingTempPath);
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
boolean changesMade =
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
if (changesMade) {
@ -79,46 +76,4 @@ public class ConfigInitializer {
log.info("Created custom_settings file: {}", customSettingsPath.toString());
}
}
// TODO: Remove post migration
private void migrateEnterpriseEditionToPremium(YamlHelper yaml, YamlHelper template) {
if (yaml.getValueByExactKeyPath("enterpriseEdition", "enabled") != null) {
template.updateValue(
List.of("premium", "enabled"),
yaml.getValueByExactKeyPath("enterpriseEdition", "enabled"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "key") != null) {
template.updateValue(
List.of("premium", "key"),
yaml.getValueByExactKeyPath("enterpriseEdition", "key"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin") != null) {
template.updateValue(
List.of("premium", "proFeatures", "SSOAutoLogin"),
yaml.getValueByExactKeyPath("enterpriseEdition", "SSOAutoLogin"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "autoUpdateMetadata")
!= null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "autoUpdateMetadata"),
yaml.getValueByExactKeyPath(
"enterpriseEdition", "CustomMetadata", "autoUpdateMetadata"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author") != null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "author"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "author"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator") != null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "creator"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "creator"));
}
if (yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer")
!= null) {
template.updateValue(
List.of("premium", "proFeatures", "CustomMetadata", "producer"),
yaml.getValueByExactKeyPath("enterpriseEdition", "CustomMetadata", "producer"));
}
}
}

View File

@ -5,9 +5,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
@ -22,14 +22,10 @@ public class EndpointConfiguration {
private final ApplicationProperties applicationProperties;
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
private final boolean runningProOrHigher;
@Autowired
public EndpointConfiguration(
ApplicationProperties applicationProperties,
@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
this.runningProOrHigher = runningProOrHigher;
init();
processEnvironmentConfigs();
}
@ -45,10 +41,6 @@ public class EndpointConfiguration {
}
}
public Map<String, Boolean> getEndpointStatuses() {
return endpointStatuses;
}
public boolean isEndpointEnabled(String endpoint) {
if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1);
@ -56,22 +48,6 @@ public class EndpointConfiguration {
return endpointStatuses.getOrDefault(endpoint, true);
}
public boolean isGroupEnabled(String group) {
Set<String> endpoints = endpointGroups.get(group);
if (endpoints == null || endpoints.isEmpty()) {
log.debug("Group '{}' does not exist or has no endpoints", group);
return false;
}
for (String endpoint : endpoints) {
if (!isEndpointEnabled(endpoint)) {
return false;
}
}
return true;
}
public void addEndpointToGroup(String group, String endpoint) {
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
}
@ -101,7 +77,7 @@ public class EndpointConfiguration {
// is false)
.map(Map.Entry::getKey)
.sorted()
.toList();
.collect(Collectors.toList());
if (!disabledList.isEmpty()) {
log.info(
@ -188,8 +164,14 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf");
addEndpointToGroup("CLI", "book-to-pdf");
addEndpointToGroup("CLI", "pdf-to-book");
addEndpointToGroup("CLI", "pdf-to-rtf");
// Calibre
addEndpointToGroup("Calibre", "book-to-pdf");
addEndpointToGroup("Calibre", "pdf-to-book");
// python
addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "html-to-pdf");
@ -200,17 +182,21 @@ public class EndpointConfiguration {
addEndpointToGroup("OpenCV", "extract-image-scans");
// LibreOffice
addEndpointToGroup("qpdf", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("LibreOffice", "pdf-to-rtf");
addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
addEndpointToGroup("LibreOffice", "pdf-to-pdfa");
// Unoconvert
addEndpointToGroup("Unoconvert", "file-to-pdf");
// qpdf
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("tesseract", "ocr-pdf");
// Java
@ -260,6 +246,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Javascript", "adjust-contrast");
// qpdf dependent endpoints
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("qpdf", "repair");
// Weasyprint dependent endpoints
@ -289,13 +277,6 @@ public class EndpointConfiguration {
}
}
}
if (!runningProOrHigher) {
disableGroup("enterprise");
}
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
disableEndpoint("url-to-pdf");
}
}
public Set<String> getEndpointsForGroup(String group) {

View File

@ -1,215 +0,0 @@
package stirling.software.SPDF.config;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
@Component
public class EndpointInspector implements ApplicationListener<ContextRefreshedEvent> {
private static final Logger logger = LoggerFactory.getLogger(EndpointInspector.class);
private final ApplicationContext applicationContext;
private final Set<String> validGetEndpoints = new HashSet<>();
private boolean endpointsDiscovered = false;
@Autowired
public EndpointInspector(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if (!endpointsDiscovered) {
discoverEndpoints();
endpointsDiscovered = true;
}
}
private void discoverEndpoints() {
try {
Map<String, RequestMappingHandlerMapping> mappings =
applicationContext.getBeansOfType(RequestMappingHandlerMapping.class);
for (Map.Entry<String, RequestMappingHandlerMapping> entry : mappings.entrySet()) {
RequestMappingHandlerMapping mapping = entry.getValue();
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> handlerEntry :
handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = handlerEntry.getKey();
HandlerMethod handlerMethod = handlerEntry.getValue();
boolean isGetHandler = false;
try {
Set<RequestMethod> methods = mappingInfo.getMethodsCondition().getMethods();
isGetHandler = methods.isEmpty() || methods.contains(RequestMethod.GET);
} catch (Exception e) {
isGetHandler = true;
}
if (isGetHandler) {
Set<String> patterns = extractPatternsUsingDirectPaths(mappingInfo);
if (patterns.isEmpty()) {
patterns = extractPatternsFromString(mappingInfo);
}
validGetEndpoints.addAll(patterns);
}
}
}
if (validGetEndpoints.isEmpty()) {
logger.warn("No endpoints discovered. Adding common endpoints as fallback.");
validGetEndpoints.add("/");
validGetEndpoints.add("/api/**");
validGetEndpoints.add("/**");
}
} catch (Exception e) {
logger.error("Error discovering endpoints", e);
}
}
private Set<String> extractPatternsUsingDirectPaths(RequestMappingInfo mappingInfo) {
Set<String> patterns = new HashSet<>();
try {
Method getDirectPathsMethod = mappingInfo.getClass().getMethod("getDirectPaths");
Object result = getDirectPathsMethod.invoke(mappingInfo);
if (result instanceof Set) {
@SuppressWarnings("unchecked")
Set<String> resultSet = (Set<String>) result;
patterns.addAll(resultSet);
}
} catch (Exception e) {
// Return empty set if method not found or fails
}
return patterns;
}
private Set<String> extractPatternsFromString(RequestMappingInfo mappingInfo) {
Set<String> patterns = new HashSet<>();
try {
String infoString = mappingInfo.toString();
if (infoString.contains("{")) {
String patternsSection =
infoString.substring(infoString.indexOf("{") + 1, infoString.indexOf("}"));
for (String pattern : patternsSection.split(",")) {
pattern = pattern.trim();
if (!pattern.isEmpty()) {
patterns.add(pattern);
}
}
}
} catch (Exception e) {
// Return empty set if parsing fails
}
return patterns;
}
public boolean isValidGetEndpoint(String uri) {
if (!endpointsDiscovered) {
discoverEndpoints();
endpointsDiscovered = true;
}
if (validGetEndpoints.contains(uri)) {
return true;
}
if (matchesWildcardOrPathVariable(uri)) {
return true;
}
if (matchesPathSegments(uri)) {
return true;
}
return false;
}
private boolean matchesWildcardOrPathVariable(String uri) {
for (String pattern : validGetEndpoints) {
if (pattern.contains("*") || pattern.contains("{")) {
int wildcardIndex = pattern.indexOf('*');
int variableIndex = pattern.indexOf('{');
int cutoffIndex;
if (wildcardIndex < 0) {
cutoffIndex = variableIndex;
} else if (variableIndex < 0) {
cutoffIndex = wildcardIndex;
} else {
cutoffIndex = Math.min(wildcardIndex, variableIndex);
}
String staticPrefix = pattern.substring(0, cutoffIndex);
if (uri.startsWith(staticPrefix)) {
return true;
}
}
}
return false;
}
private boolean matchesPathSegments(String uri) {
for (String pattern : validGetEndpoints) {
if (!pattern.contains("*") && !pattern.contains("{")) {
String[] patternSegments = pattern.split("/");
String[] uriSegments = uri.split("/");
if (uriSegments.length < patternSegments.length) {
continue;
}
boolean match = true;
for (int i = 0; i < patternSegments.length; i++) {
if (!patternSegments[i].equals(uriSegments[i])) {
match = false;
break;
}
}
if (match) {
return true;
}
}
}
return false;
}
public Set<String> getValidGetEndpoints() {
if (!endpointsDiscovered) {
discoverEndpoints();
endpointsDiscovered = true;
}
return new HashSet<>(validGetEndpoints);
}
private void logAllEndpoints() {
Set<String> sortedEndpoints = new TreeSet<>(validGetEndpoints);
logger.info("=== BEGIN: All discovered GET endpoints ===");
for (String endpoint : sortedEndpoints) {
logger.info("Endpoint: {}", endpoint);
}
logger.info("=== END: All discovered GET endpoints ===");
}
}

View File

@ -6,10 +6,7 @@ import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Component
@Slf4j
public class EndpointInterceptor implements HandlerInterceptor {
private final EndpointConfiguration endpointConfiguration;
@ -23,29 +20,7 @@ public class EndpointInterceptor implements HandlerInterceptor {
HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String requestURI = request.getRequestURI();
boolean isEnabled;
// Extract the specific endpoint name (e.g: /api/v1/general/remove-pages -> remove-pages)
if (requestURI.contains("/api/v1") && requestURI.split("/").length > 4) {
String[] requestURIParts = requestURI.split("/");
String requestEndpoint;
// Endpoint: /api/v1/convert/pdf/img becomes pdf-to-img
if ("convert".equals(requestURIParts[3]) && requestURIParts.length > 5) {
requestEndpoint = requestURIParts[4] + "-to-" + requestURIParts[5];
} else {
requestEndpoint = requestURIParts[4];
}
log.debug("Request endpoint: {}", requestEndpoint);
isEnabled = endpointConfiguration.isEndpointEnabled(requestEndpoint);
log.debug("Is endpoint enabled: {}", isEnabled);
} else {
isEnabled = endpointConfiguration.isEndpointEnabled(requestURI);
}
if (!isEnabled) {
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false;
}

View File

@ -1,38 +0,0 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class EnterpriseEndpointFilter extends OncePerRequestFilter {
private final boolean runningProOrHigher;
public EnterpriseEndpointFilter(@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
this.runningProOrHigher = runningProOrHigher;
}
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!runningProOrHigher && isPrometheusEndpointRequest(request)) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
filterChain.doFilter(request, response);
}
private boolean isPrometheusEndpointRequest(HttpServletRequest request) {
return request.getRequestURI().contains("/actuator/");
}
}

View File

@ -62,7 +62,7 @@ public class ExternalAppDepConfig {
private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint))
.toList();
.collect(Collectors.toList());
}
private String formatEndpointAsFeature(String endpoint) {

View File

@ -2,6 +2,7 @@ package stirling.software.SPDF.config.security;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority;
@ -57,6 +58,6 @@ public class CustomUserDetailsService implements UserDetailsService {
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.toList();
.collect(Collectors.toList());
}
}

View File

@ -46,13 +46,13 @@ import stirling.software.SPDF.repository.PersistentLoginRepository;
@EnableWebSecurity
@EnableMethodSecurity
@Slf4j
@DependsOn("runningProOrHigher")
@DependsOn("runningEE")
public class SecurityConfiguration {
private final CustomUserDetailsService userDetailsService;
private final UserService userService;
private final boolean loginEnabledValue;
private final boolean runningProOrHigher;
private final boolean runningEE;
private final ApplicationProperties applicationProperties;
private final UserAuthenticationFilter userAuthenticationFilter;
@ -69,7 +69,7 @@ public class SecurityConfiguration {
CustomUserDetailsService userDetailsService,
@Lazy UserService userService,
@Qualifier("loginEnabled") boolean loginEnabledValue,
@Qualifier("runningProOrHigher") boolean runningProOrHigher,
@Qualifier("runningEE") boolean runningEE,
ApplicationProperties applicationProperties,
UserAuthenticationFilter userAuthenticationFilter,
LoginAttemptService loginAttemptService,
@ -83,7 +83,7 @@ public class SecurityConfiguration {
this.userDetailsService = userDetailsService;
this.userService = userService;
this.loginEnabledValue = loginEnabledValue;
this.runningProOrHigher = runningProOrHigher;
this.runningEE = runningEE;
this.applicationProperties = applicationProperties;
this.userAuthenticationFilter = userAuthenticationFilter;
this.loginAttemptService = loginAttemptService;
@ -254,7 +254,7 @@ public class SecurityConfiguration {
.permitAll());
}
// Handle SAML
if (applicationProperties.getSecurity().isSaml2Active() && runningProOrHigher) {
if (applicationProperties.getSecurity().isSaml2Active() && runningEE) {
// Configure the authentication provider
OpenSaml4AuthenticationProvider authenticationProvider =
new OpenSaml4AuthenticationProvider();

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
@ -98,7 +99,7 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
authority ->
new SimpleGrantedAuthority(
authority.getAuthority()))
.toList();
.collect(Collectors.toList());
authentication = new ApiKeyAuthenticationToken(user.get(), apiKey, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) {

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.config.security;
import java.io.IOException;
import java.sql.SQLException;
import java.util.*;
import java.util.stream.Collectors;
import org.springframework.context.MessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
@ -107,7 +108,7 @@ public class UserService implements UserServiceInterface {
// Convert each Authority object into a SimpleGrantedAuthority object.
return user.getAuthorities().stream()
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
.toList();
.collect(Collectors.toList());
}
private String generateApiKey() {
@ -205,7 +206,6 @@ public class UserService implements UserServiceInterface {
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
user.setAuthenticationType(AuthenticationType.WEB);
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
userRepository.save(user);
databaseService.exportDatabase();
}
@ -231,22 +231,6 @@ public class UserService implements UserServiceInterface {
saveUser(username, password, role, false);
}
public void saveUser(String username, String password, boolean firstLogin, boolean enabled)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!isUsernameValid(username)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(Role.USER.getRoleId(), user));
user.setEnabled(enabled);
user.setAuthenticationType(AuthenticationType.WEB);
user.setFirstLogin(firstLogin);
userRepository.save(user);
databaseService.exportDatabase();
}
public void deleteUser(String username) {
Optional<User> userOpt = findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
@ -369,7 +353,6 @@ public class UserService implements UserServiceInterface {
List<String> notAllowedUserList = new ArrayList<>();
notAllowedUserList.add("ALL_USERS".toLowerCase());
notAllowedUserList.add("anonymoususer");
boolean notAllowedUser = notAllowedUserList.contains(username.toLowerCase());
return (isValidSimpleUsername || isValidEmail) && !notAllowedUser;
}
@ -475,12 +458,6 @@ public class UserService implements UserServiceInterface {
@Override
public long getTotalUsersCount() {
// Count all users in the database
long userCount = userRepository.count();
// Exclude the internal API user from the count
if (findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId()).isPresent()) {
userCount -= 1;
}
return userCount;
return userRepository.count();
}
}

View File

@ -27,18 +27,18 @@ public class DatabaseConfig {
public static final String POSTGRES_DRIVER = "org.postgresql.Driver";
private final ApplicationProperties applicationProperties;
private final boolean runningProOrHigher;
private final boolean runningEE;
public DatabaseConfig(
ApplicationProperties applicationProperties,
@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
@Qualifier("runningEE") boolean runningEE) {
DATASOURCE_DEFAULT_URL =
"jdbc:h2:file:"
+ InstallationPathConfig.getConfigPath()
+ "stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
log.debug("Database URL: {}", DATASOURCE_DEFAULT_URL);
this.applicationProperties = applicationProperties;
this.runningProOrHigher = runningProOrHigher;
this.runningEE = runningEE;
}
/**
@ -54,7 +54,7 @@ public class DatabaseConfig {
public DataSource dataSource() throws UnsupportedProviderException {
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
if (!runningProOrHigher) {
if (!runningEE) {
return useDefaultDataSource(dataSourceBuilder);
}

View File

@ -186,6 +186,7 @@ public class OAuth2Configuration {
oauth.getClientSecret(),
oauth.getScopes(),
UsernameAttribute.valueOf(oauth.getUseAsUsername().toUpperCase()),
oauth.getLogoutUrl(),
null,
null,
null);

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.util.*;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
@ -11,33 +12,24 @@ import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.encryption.PDEncryption;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
@RestController
@RequestMapping("/api/v1/analysis")
@Tag(name = "Analysis", description = "Analysis APIs")
public class AnalysisController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public AnalysisController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(value = "/page-count", consumes = "multipart/form-data")
@Operation(
summary = "Get PDF page count",
description = "Returns total number of pages in PDF. Input:PDF Output:JSON Type:SISO")
public Map<String, Integer> getPageCount(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
return Map.of("pageCount", document.getNumberOfPages());
}
}
@ -47,7 +39,7 @@ public class AnalysisController {
summary = "Get basic PDF information",
description = "Returns page count, version, file size. Input:PDF Output:JSON Type:SISO")
public Map<String, Object> getBasicInfo(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
Map<String, Object> info = new HashMap<>();
info.put("pageCount", document.getNumberOfPages());
info.put("pdfVersion", document.getVersion());
@ -62,7 +54,7 @@ public class AnalysisController {
description = "Returns title, author, subject, etc. Input:PDF Output:JSON Type:SISO")
public Map<String, String> getDocumentProperties(@ModelAttribute PDFFile file)
throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
PDDocumentInformation info = document.getDocumentInformation();
Map<String, String> properties = new HashMap<>();
properties.put("title", info.getTitle());
@ -83,7 +75,7 @@ public class AnalysisController {
description = "Returns width and height of each page. Input:PDF Output:JSON Type:SISO")
public List<Map<String, Float>> getPageDimensions(@ModelAttribute PDFFile file)
throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
List<Map<String, Float>> dimensions = new ArrayList<>();
PDPageTree pages = document.getPages();
@ -103,7 +95,7 @@ public class AnalysisController {
description =
"Returns count and details of form fields. Input:PDF Output:JSON Type:SISO")
public Map<String, Object> getFormFields(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
Map<String, Object> formInfo = new HashMap<>();
PDAcroForm form = document.getDocumentCatalog().getAcroForm();
@ -125,7 +117,7 @@ public class AnalysisController {
summary = "Get annotation information",
description = "Returns count and types of annotations. Input:PDF Output:JSON Type:SISO")
public Map<String, Object> getAnnotationInfo(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
Map<String, Object> annotInfo = new HashMap<>();
int totalAnnotations = 0;
Map<String, Integer> annotationTypes = new HashMap<>();
@ -150,7 +142,7 @@ public class AnalysisController {
description =
"Returns list of fonts used in the document. Input:PDF Output:JSON Type:SISO")
public Map<String, Object> getFontInfo(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
Map<String, Object> fontInfo = new HashMap<>();
Set<String> fontNames = new HashSet<>();
@ -172,7 +164,7 @@ public class AnalysisController {
description =
"Returns encryption and permission details. Input:PDF Output:JSON Type:SISO")
public Map<String, Object> getSecurityInfo(@ModelAttribute PDFFile file) throws IOException {
try (PDDocument document = pdfDocumentFactory.load(file.getFileInput())) {
try (PDDocument document = Loader.loadPDF(file.getFileInput().getBytes())) {
Map<String, Object> securityInfo = new HashMap<>();
PDEncryption encryption = document.getEncryption();

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -21,7 +22,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.CropPdfForm;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.service.PostHogService;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -29,21 +31,24 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class CropController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
private final PostHogService postHogService;
@Autowired
public CropController(CustomPDFDocumentFactory pdfDocumentFactory) {
public CropController(
CustomPDDocumentFactory pdfDocumentFactory, PostHogService postHogService) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.postHogService = postHogService;
}
@PostMapping(value = "/crop", consumes = "multipart/form-data")
@Operation(
summary = "Crops a PDF document",
description =
"This operation takes an input PDF file and crops it according to the given"
+ " coordinates. Input:PDF Output:PDF Type:SISO")
"This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException {
PDDocument sourceDocument = pdfDocumentFactory.load(form);
PDDocument sourceDocument = Loader.loadPDF(form.getFileInput().getBytes());
PDDocument newDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);

View File

@ -10,7 +10,9 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
@ -32,7 +34,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -42,10 +44,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class MergeController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public MergeController(CustomPDFDocumentFactory pdfDocumentFactory) {
public MergeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -99,8 +101,8 @@ public class MergeController {
};
case "byPDFTitle":
return (file1, file2) -> {
try (PDDocument doc1 = pdfDocumentFactory.load(file1);
PDDocument doc2 = pdfDocumentFactory.load(file2)) {
try (PDDocument doc1 = Loader.loadPDF(file1.getBytes());
PDDocument doc2 = Loader.loadPDF(file2.getBytes())) {
String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2);
@ -118,13 +120,12 @@ public class MergeController {
@Operation(
summary = "Merge multiple PDF files into one",
description =
"This endpoint merges multiple PDF files into a single PDF file. The merged"
+ " file will contain all pages from the input files in the order they were"
+ " provided. Input:PDF Output:PDF Type:MISO")
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
File mergedTempFile = null;
ByteArrayOutputStream docOutputstream =
new ByteArrayOutputStream(); // Stream for the merged document
PDDocument mergedDocument = null;
boolean removeCertSign = form.isRemoveCertSign();
@ -137,24 +138,21 @@ public class MergeController {
form.getSortType())); // Sort files based on the given sort type
PDFMergerUtility mergerUtility = new PDFMergerUtility();
long totalSize = 0;
for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize();
File tempFile =
GeneralUtils.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility
}
mergerUtility.setDestinationStream(
docOutputstream); // Set the output stream for the merged document
mergerUtility.mergeDocuments(null); // Merge the documents
mergedTempFile = Files.createTempFile("merged-", ".pdf").toFile();
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
// Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedTempFile);
mergedDocument = Loader.loadPDF(mergedPdfBytes);
// Remove signatures if removeCertSign is true
if (removeCertSign) {
@ -164,7 +162,7 @@ public class MergeController {
List<PDField> fieldsToRemove =
acroForm.getFields().stream()
.filter(field -> field instanceof PDSignatureField)
.toList();
.collect(Collectors.toList());
if (!fieldsToRemove.isEmpty()) {
acroForm.flatten(
@ -181,23 +179,21 @@ public class MergeController {
String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf";
return WebResponseUtils.boasToWebResponse(
baos, mergedFileName); // Return the modified PDF
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), mergedFileName); // Return the modified PDF
} catch (Exception ex) {
log.error("Error in merge pdf process", ex);
throw ex;
} finally {
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
for (File file : filesToDelete) {
if (file != null) {
Files.deleteIfExists(file.toPath()); // Delete temporary files
}
}
if (mergedTempFile != null) {
Files.deleteIfExists(mergedTempFile.toPath());
docOutputstream.close();
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
}
}

View File

@ -4,6 +4,7 @@ import java.awt.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -24,7 +25,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -32,10 +33,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public MultiPageLayoutController(CustomPDFDocumentFactory pdfDocumentFactory) {
public MultiPageLayoutController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -43,8 +44,7 @@ public class MultiPageLayoutController {
@Operation(
summary = "Merge multiple pages of a PDF document into a single page",
description =
"This operation takes an input PDF file and the number of pages to merge into a"
+ " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
"This operation takes an input PDF file and the number of pages to merge into a single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@ModelAttribute MergeMultiplePagesRequest request) throws IOException {
@ -64,7 +64,7 @@ public class MultiPageLayoutController {
: (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
PDDocument newDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
PDPage newPage = new PDPage(PDRectangle.A4);

View File

@ -15,7 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.service.PdfImageRemovalService;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -31,7 +31,7 @@ public class PdfImageRemovalController {
// Service for removing images from PDFs
private final PdfImageRemovalService pdfImageRemovalService;
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
/**
* Constructor for dependency injection of PdfImageRemovalService.
@ -41,7 +41,7 @@ public class PdfImageRemovalController {
@Autowired
public PdfImageRemovalController(
PdfImageRemovalService pdfImageRemovalService,
CustomPDFDocumentFactory pdfDocumentFactory) {
CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfImageRemovalService = pdfImageRemovalService;
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -61,8 +61,7 @@ public class PdfImageRemovalController {
@Operation(
summary = "Remove images from file to reduce the file size.",
description =
"This endpoint remove images from file to reduce the file size.Input:PDF"
+ " Output:PDF Type:MISO")
"This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
// Load the PDF document
PDDocument document = pdfDocumentFactory.load(file);

View File

@ -26,7 +26,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -35,10 +35,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class PdfOverlayController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public PdfOverlayController(CustomPDFDocumentFactory pdfDocumentFactory) {
public PdfOverlayController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -46,8 +46,7 @@ public class PdfOverlayController {
@Operation(
summary = "Overlay PDF files in various modes",
description =
"Overlay PDF files onto a base PDF with different modes: Sequential,"
+ " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
"Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
throws IOException {
MultipartFile baseFile = request.getFileInput();

View File

@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired;
@ -24,7 +25,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,10 +35,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public RearrangePagesPDFController(CustomPDFDocumentFactory pdfDocumentFactory) {
public RearrangePagesPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -45,9 +46,7 @@ public class RearrangePagesPDFController {
@Operation(
summary = "Remove pages from a PDF file",
description =
"This endpoint removes specified pages from a given PDF file. Users can provide"
+ " a comma-separated list of page numbers or ranges to delete. Input:PDF"
+ " Output:PDF Type:SISO")
"This endpoint removes specified pages from a given PDF file. Users can provide a comma-separated list of page numbers or ranges to delete. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
throws IOException {
@ -244,10 +243,7 @@ public class RearrangePagesPDFController {
@Operation(
summary = "Rearrange pages in a PDF file",
description =
"This endpoint rearranges pages in a given PDF file based on the specified page"
+ " order or custom mode. Users can provide a page order as a"
+ " comma-separated list of page numbers or page ranges, or a custom mode."
+ " Input:PDF Output:PDF")
"This endpoint rearranges pages in a given PDF file based on the specified page order or custom mode. Users can provide a page order as a comma-separated list of page numbers or page ranges, or a custom mode. Input:PDF Output:PDF")
public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
@ -255,7 +251,7 @@ public class RearrangePagesPDFController {
String sortType = request.getCustomMode();
try {
// Load the input PDF
PDDocument document = pdfDocumentFactory.load(pdfFile);
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
// Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];

View File

@ -18,7 +18,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -26,10 +26,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class RotationController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public RotationController(CustomPDFDocumentFactory pdfDocumentFactory) {
public RotationController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -37,18 +37,11 @@ public class RotationController {
@Operation(
summary = "Rotate a PDF file",
description =
"This endpoint rotates a given PDF file by a specified angle. The angle must be"
+ " a multiple of 90. Input:PDF Output:PDF Type:SISO")
"This endpoint rotates a given PDF file by a specified angle. The angle must be a multiple of 90. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
throws IOException {
MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle();
// Validate the angle is a multiple of 90
if (angle % 90 != 0) {
throw new IllegalArgumentException("Angle must be a multiple of 90");
}
// Load the PDF document
PDDocument document = pdfDocumentFactory.load(request);

View File

@ -5,6 +5,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -25,7 +26,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -33,10 +34,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class ScalePagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ScalePagesController(CustomPDFDocumentFactory pdfDocumentFactory) {
public ScalePagesController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -44,15 +45,14 @@ public class ScalePagesController {
@Operation(
summary = "Change the size of a PDF page/document",
description =
"This operation takes an input PDF file and the size to scale the pages to in"
+ " the output PDF file. Input:PDF Output:PDF Type:SISO")
"This operation takes an input PDF file and the size to scale the pages to in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> scalePages(@ModelAttribute ScalePagesRequest request)
throws IOException {
MultipartFile file = request.getFileInput();
String targetPDRectangle = request.getPageSize();
float scaleFactor = request.getScaleFactor();
PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
PDDocument outputDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
@ -124,8 +124,7 @@ public class ScalePagesController {
}
throw new IllegalArgumentException(
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6,"
+ " LETTER, LEGAL, KEEP");
"Invalid PDRectangle. It must be one of the following: A0, A1, A2, A3, A4, A5, A6, LETTER, LEGAL, KEEP");
}
private Map<String, PDRectangle> getSizeMap() {

View File

@ -1,12 +1,10 @@
package stirling.software.SPDF.controller.api;
import java.io.IOException;
import java.util.Map;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@ -14,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils;
@ -26,13 +23,9 @@ import stirling.software.SPDF.utils.GeneralUtils;
public class SettingsController {
private final ApplicationProperties applicationProperties;
private final EndpointConfiguration endpointConfiguration;
public SettingsController(
ApplicationProperties applicationProperties,
EndpointConfiguration endpointConfiguration) {
public SettingsController(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
this.endpointConfiguration = endpointConfiguration;
}
@PostMapping("/update-enable-analytics")
@ -48,10 +41,4 @@ public class SettingsController {
applicationProperties.getSystem().setEnableAnalytics(enabled);
return ResponseEntity.ok("Updated");
}
@GetMapping("/get-endpoints-status")
@Hidden
public ResponseEntity<Map<String, Boolean>> getDisabledEndpoints() {
return ResponseEntity.ok(endpointConfiguration.getEndpointStatuses());
}
}

View File

@ -10,6 +10,7 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired;
@ -28,7 +29,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -37,10 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class SplitPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public SplitPDFController(CustomPDFDocumentFactory pdfDocumentFactory) {
public SplitPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -48,10 +49,7 @@ public class SplitPDFController {
@Operation(
summary = "Split a PDF file into separate documents",
description =
"This endpoint splits a given PDF file into separate documents based on the"
+ " specified page numbers or ranges. Users can specify pages using"
+ " individual numbers, ranges, or 'all' for every page. Input:PDF"
+ " Output:PDF Type:SIMO")
"This endpoint splits a given PDF file into separate documents based on the specified page numbers or ranges. Users can specify pages using individual numbers, ranges, or 'all' for every page. Input:PDF Output:PDF Type:SIMO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException {
@ -65,7 +63,7 @@ public class SplitPDFController {
String pages = request.getPageNumbers();
// open the pdf document
document = pdfDocumentFactory.load(file);
document = Loader.loadPDF(file.getBytes());
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false);

View File

@ -8,6 +8,7 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
@ -33,7 +34,6 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.PdfMetadata;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.PdfMetadataService;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -45,13 +45,9 @@ public class SplitPdfByChaptersController {
private final PdfMetadataService pdfMetadataService;
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public SplitPdfByChaptersController(
PdfMetadataService pdfMetadataService, CustomPDFDocumentFactory pdfDocumentFactory) {
public SplitPdfByChaptersController(PdfMetadataService pdfMetadataService) {
this.pdfMetadataService = pdfMetadataService;
this.pdfDocumentFactory = pdfDocumentFactory;
}
private static List<Bookmark> extractOutlineItems(
@ -139,7 +135,7 @@ public class SplitPdfByChaptersController {
if (bookmarkLevel < 0) {
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
}
sourceDocument = pdfDocumentFactory.load(file);
sourceDocument = Loader.loadPDF(file.getBytes());
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();

View File

@ -9,6 +9,7 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -31,7 +32,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -39,10 +40,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public SplitPdfBySectionsController(CustomPDFDocumentFactory pdfDocumentFactory) {
public SplitPdfBySectionsController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -50,15 +51,13 @@ public class SplitPdfBySectionsController {
@Operation(
summary = "Split PDF pages into smaller sections",
description =
"Split each page of a PDF into smaller sections based on the user's choice"
+ " (halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO")
"Split each page of a PDF into smaller sections based on the user's choice (halves, thirds, quarters, etc.), both vertically and horizontally. Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput();
PDDocument sourceDocument = pdfDocumentFactory.load(file);
PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
// Process the PDF based on split parameters
int horiz = request.getHorizontalDivisions() + 1;

View File

@ -7,6 +7,7 @@ import java.nio.file.Path;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired;
@ -25,7 +26,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -35,10 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public SplitPdfBySizeController(CustomPDFDocumentFactory pdfDocumentFactory) {
public SplitPdfBySizeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -46,96 +47,43 @@ public class SplitPdfBySizeController {
@Operation(
summary = "Auto split PDF pages into separate documents based on size or count",
description =
"split PDF into multiple paged documents based on size/count, ie if 20 pages"
+ " and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB"
+ " (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF"
+ " Output:ZIP-PDF Type:SISO")
"split PDF into multiple paged documents based on size/count, ie if 20 pages and split into 5, it does 5 documents each 4 pages\r\n"
+ " if 10MB and each page is 1MB and you enter 2MB then 5 docs each 2MB (rounded so that it accepts 1.9MB but not 2.1MB) Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception {
log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput();
Path zipFile = Files.createTempFile("split_documents", ".zip");
log.debug("Created temporary zip file: {}", zipFile);
String filename =
Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", "");
log.debug("Base filename for output: {}", filename);
byte[] data = null;
try {
log.debug("Reading input file bytes");
byte[] pdfBytes = file.getBytes();
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
log.debug("Creating ZIP output stream");
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) {
log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.debug(
"Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages());
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile));
PDDocument sourceDocument = Loader.loadPDF(file.getBytes())) {
int type = request.getSplitType();
String value = request.getSplitValue();
log.debug("Split type: {}, Split value: {}", type, value);
if (type == 0) {
log.debug("Processing split by size");
long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.debug("Max bytes per document: {}", maxBytes);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
log.debug("Processing split by page count");
int pageCount = Integer.parseInt(value);
log.debug("Pages per document: {}", pageCount);
handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
} else if (type == 2) {
log.debug("Processing split by document count");
int documentCount = Integer.parseInt(value);
log.debug("Total number of documents: {}", documentCount);
handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else {
log.error("Invalid split type: {}", type);
throw new IllegalArgumentException(
"Invalid argument for split type: " + type);
}
log.debug("PDF splitting completed successfully");
} catch (Exception e) {
log.error("Error loading or processing PDF document", e);
throw e;
}
} catch (IOException e) {
log.error("Error creating or writing to ZIP file", e);
throw e;
throw new IllegalArgumentException("Invalid argument for split type");
}
} catch (Exception e) {
log.error("Exception during PDF splitting process", e);
throw e; // Re-throw to ensure proper error response
log.error("exception", e);
} finally {
try {
log.debug("Reading ZIP file data");
data = Files.readAllBytes(zipFile);
log.debug("Successfully read {} bytes from ZIP file", data.length);
} catch (IOException e) {
log.error("Error reading ZIP file data", e);
Files.deleteIfExists(zipFile);
}
try {
log.debug("Deleting temporary ZIP file");
boolean deleted = Files.deleteIfExists(zipFile);
log.debug("Temporary ZIP file deleted: {}", deleted);
} catch (IOException e) {
log.error("Error deleting temporary ZIP file", e);
}
}
log.debug("Returning response with {} bytes of data", data != null ? data.length : 0);
return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
}
@ -143,230 +91,63 @@ public class SplitPdfBySizeController {
private void handleSplitBySize(
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes);
long currentSize = 0;
PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
int fileIndex = 1;
int totalPages = sourceDocument.getNumberOfPages();
int pageAdded = 0;
// Smart size check frequency - check more often with larger documents
int baseCheckFrequency = 5;
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
for (int pageIndex = 0; pageIndex < sourceDocument.getNumberOfPages(); pageIndex++) {
PDPage page = sourceDocument.getPage(pageIndex);
log.debug("Processing page {} of {}", pageIndex + 1, totalPages);
ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
// Add the page to current document
PDPage newPage = new PDPage(page.getCOSObject());
currentDoc.addPage(newPage);
pageAdded++;
// Dynamic size checking based on document size and page count
boolean shouldCheckSize =
(pageAdded % baseCheckFrequency == 0)
|| (pageIndex == totalPages - 1)
|| (pageAdded >= 20); // Always check after 20 pages
if (shouldCheckSize) {
log.debug("Performing size check after {} pages", pageAdded);
ByteArrayOutputStream checkSizeStream = new ByteArrayOutputStream();
currentDoc.save(checkSizeStream);
long actualSize = checkSizeStream.size();
log.debug("Current document size: {} bytes (max: {} bytes)", actualSize, maxBytes);
if (actualSize > maxBytes) {
// We exceeded the limit - remove the last page and save
if (currentDoc.getNumberOfPages() > 1) {
currentDoc.removePage(currentDoc.getNumberOfPages() - 1);
pageIndex--; // Process this page again in the next document
log.debug("Size limit exceeded - removed last page");
try (PDDocument tempDoc = new PDDocument()) {
PDPage importedPage = tempDoc.importPage(page); // This creates a new PDPage object
tempDoc.save(pageOutputStream);
}
log.debug(
"Saving document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc = new PDDocument();
pageAdded = 0;
} else if (pageIndex < totalPages - 1) {
// We're under the limit, calculate if we might fit more pages
// Try to predict how many more similar pages might fit
if (actualSize < maxBytes * 0.75 && pageAdded > 0) {
// Rather than using a ratio, look ahead to test actual upcoming pages
int pagesToLookAhead = Math.min(5, totalPages - pageIndex - 1);
if (pagesToLookAhead > 0) {
log.debug(
"Testing {} upcoming pages for potential addition",
pagesToLookAhead);
// Create a temp document with current pages + look-ahead pages
PDDocument testDoc = new PDDocument();
// First copy existing pages
for (int i = 0; i < currentDoc.getNumberOfPages(); i++) {
testDoc.addPage(new PDPage(currentDoc.getPage(i).getCOSObject()));
}
// Try adding look-ahead pages one by one
int extraPagesAdded = 0;
for (int i = 0; i < pagesToLookAhead; i++) {
int testPageIndex = pageIndex + 1 + i;
PDPage testPage = sourceDocument.getPage(testPageIndex);
testDoc.addPage(new PDPage(testPage.getCOSObject()));
// Check if we're still under size
ByteArrayOutputStream testStream = new ByteArrayOutputStream();
testDoc.save(testStream);
long testSize = testStream.size();
if (testSize <= maxBytes) {
extraPagesAdded++;
log.debug(
"Test: Can add page {} (size would be {})",
testPageIndex + 1,
testSize);
} else {
log.debug(
"Test: Cannot add page {} (size would be {})",
testPageIndex + 1,
testSize);
break;
}
}
testDoc.close();
// Add the pages we verified would fit
if (extraPagesAdded > 0) {
log.debug("Adding {} verified pages ahead", extraPagesAdded);
for (int i = 0; i < extraPagesAdded; i++) {
int extraPageIndex = pageIndex + 1 + i;
PDPage extraPage = sourceDocument.getPage(extraPageIndex);
currentDoc.addPage(new PDPage(extraPage.getCOSObject()));
}
pageIndex += extraPagesAdded;
pageAdded += extraPagesAdded;
}
}
}
}
}
}
// Save final document if it has any pages
long pageSize = pageOutputStream.size();
if (currentSize + pageSize > maxBytes) {
if (currentDoc.getNumberOfPages() > 0) {
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc.close(); // Make sure to close the document
currentDoc = new PDDocument();
currentSize = 0;
}
}
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
PDPage newPage = new PDPage(page.getCOSObject()); // Re-create the page
currentDoc.addPage(newPage);
currentSize += pageSize;
}
if (currentDoc.getNumberOfPages() != 0) {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc.close();
}
}
private void handleSplitByPageCount(
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
throws IOException {
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
int currentPageCount = 0;
log.debug("Creating initial output document");
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.debug("Successfully created initial output document");
} catch (Exception e) {
log.error("Error creating initial output document", e);
throw new IOException("Failed to create initial output document", e);
}
PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
int fileIndex = 1;
int pageIndex = 0;
int totalPages = sourceDocument.getNumberOfPages();
log.debug("Processing {} pages", totalPages);
try {
for (PDPage page : sourceDocument.getPages()) {
pageIndex++;
log.debug("Processing page {} of {}", pageIndex, totalPages);
try {
log.debug("Adding page {} to current document", pageIndex);
currentDoc.addPage(page);
log.debug("Successfully added page {} to current document", pageIndex);
} catch (Exception e) {
log.error("Error adding page {} to current document", pageIndex, e);
throw new IOException("Failed to add page to document", e);
}
currentPageCount++;
log.debug("Current page count: {}/{}", currentPageCount, pageCount);
if (currentPageCount == pageCount) {
log.debug(
"Reached target page count ({}), saving current document as part {}",
pageCount,
fileIndex);
try {
// Save and reset current document
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.debug("Successfully saved document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving document part {}", fileIndex - 1, e);
throw e;
}
try {
log.debug("Creating new document for next part");
currentDoc = new PDDocument();
log.debug("Successfully created new document");
} catch (Exception e) {
log.error("Error creating new document for next part", e);
throw new IOException("Failed to create new document", e);
}
currentPageCount = 0;
log.debug("Reset current page count to 0");
}
}
} catch (Exception e) {
log.error("Error iterating through pages", e);
throw new IOException("Failed to iterate through pages", e);
}
// Add the last document if it contains any pages
try {
if (currentDoc.getPages().getCount() != 0) {
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getPages().getCount(),
fileIndex);
try {
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.debug("Successfully saved final document part {}", fileIndex - 1);
} catch (Exception e) {
log.error("Error saving final document part {}", fileIndex - 1, e);
throw e;
}
} else {
log.debug("Final document has no pages, skipping");
}
} catch (Exception e) {
log.error("Error checking or saving final document", e);
throw new IOException("Failed to process final document", e);
} finally {
try {
log.debug("Closing final document");
currentDoc.close();
log.debug("Successfully closed final document");
} catch (Exception e) {
log.error("Error closing final document", e);
}
}
log.debug("Completed handleSplitByPageCount with {} document parts created", fileIndex - 1);
}
private void handleSplitByDocCount(
@ -375,101 +156,35 @@ public class SplitPdfBySizeController {
ZipOutputStream zipOut,
String baseFilename)
throws IOException {
log.debug("Starting handleSplitByDocCount with documentCount={}", documentCount);
int totalPageCount = sourceDocument.getNumberOfPages();
log.debug("Total pages in source document: {}", totalPageCount);
int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount;
log.debug("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
int currentPageIndex = 0;
int fileIndex = 1;
for (int i = 0; i < documentCount; i++) {
log.debug("Creating document {} of {}", i + 1, documentCount);
PDDocument currentDoc = null;
try {
currentDoc = pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
log.debug("Successfully created document {} of {}", i + 1, documentCount);
} catch (Exception e) {
log.error("Error creating document {} of {}", i + 1, documentCount, e);
throw new IOException("Failed to create document", e);
}
PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
for (int j = 0; j < pagesToAdd; j++) {
try {
log.debug(
"Adding page {} (index {}) to document {}",
j + 1,
currentPageIndex,
i + 1);
currentDoc.addPage(sourceDocument.getPage(currentPageIndex));
log.debug("Successfully added page {} to document {}", j + 1, i + 1);
currentPageIndex++;
} catch (Exception e) {
log.error("Error adding page {} to document {}", j + 1, i + 1, e);
throw new IOException("Failed to add page to document", e);
}
currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
}
try {
log.debug("Saving document {} with {} pages", i + 1, pagesToAdd);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.debug("Successfully saved document {}", i + 1);
} catch (Exception e) {
log.error("Error saving document {}", i + 1, e);
throw e;
}
}
log.debug("Completed handleSplitByDocCount with {} documents created", documentCount);
}
private void saveDocumentToZip(
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
throws IOException {
log.debug("Starting saveDocumentToZip for document part {}", index);
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
try {
log.debug("Saving document part {} to byte array", index);
document.save(outStream);
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size());
} catch (Exception e) {
log.error("Error saving document part {} to byte array", index, e);
throw new IOException("Failed to save document to byte array", e);
}
document.close(); // Close the document to free resources
try {
log.debug("Closing document part {}", index);
document.close();
log.debug("Successfully closed document part {}", index);
} catch (Exception e) {
log.error("Error closing document part {}", index, e);
// Continue despite close error
}
try {
// Create a new zip entry
String entryName = baseFilename + "_" + index + ".pdf";
log.debug("Creating ZIP entry: {}", entryName);
ZipEntry zipEntry = new ZipEntry(entryName);
ZipEntry zipEntry = new ZipEntry(baseFilename + "_" + index + ".pdf");
zipOut.putNextEntry(zipEntry);
byte[] bytes = outStream.toByteArray();
log.debug("Writing {} bytes to ZIP entry", bytes.length);
zipOut.write(bytes);
log.debug("Closing ZIP entry");
zipOut.write(outStream.toByteArray());
zipOut.closeEntry();
log.debug("Successfully added document part {} to ZIP", index);
} catch (Exception e) {
log.error("Error adding document part {} to ZIP", index, e);
throw new IOException("Failed to add document to ZIP file", e);
}
}
}

View File

@ -4,6 +4,7 @@ import java.awt.geom.AffineTransform;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
@ -21,7 +22,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -29,10 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs")
public class ToSinglePageController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ToSinglePageController(CustomPDFDocumentFactory pdfDocumentFactory) {
public ToSinglePageController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -40,15 +41,12 @@ public class ToSinglePageController {
@Operation(
summary = "Convert a multi-page PDF into a single long page PDF",
description =
"This endpoint converts a multi-page PDF document into a single paged PDF"
+ " document. The width of the single page will be same as the input's"
+ " width, but the height will be the sum of all the pages' heights."
+ " Input:PDF Output:PDF Type:SISO")
"This endpoint converts a multi-page PDF document into a single paged PDF document. The width of the single page will be same as the input's width, but the height will be the sum of all the pages' heights. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException {
// Load the source document
PDDocument sourceDocument = pdfDocumentFactory.load(request);
PDDocument sourceDocument = Loader.loadPDF(request.getFileInput().getBytes());
// Calculate total height and max width
float totalHeight = 0;

View File

@ -32,7 +32,6 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User;
@ -48,15 +47,10 @@ public class UserController {
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
private final UserService userService;
private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
public UserController(
UserService userService,
SessionPersistentRegistry sessionRegistry,
ApplicationProperties applicationProperties) {
public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
this.userService = userService;
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties;
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@ -200,44 +194,39 @@ public class UserController {
boolean forceChange)
throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) {
return new RedirectView("/adminSettings?messageType=invalidUsername", true);
}
if (applicationProperties.getPremium().isEnabled()
&& applicationProperties.getPremium().getMaxUsers()
<= userService.getTotalUsersCount()) {
return new RedirectView("/adminSettings?messageType=maxUsersReached", true);
return new RedirectView("/addUsers?messageType=invalidUsername", true);
}
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=usernameExists", true);
return new RedirectView("/addUsers?messageType=usernameExists", true);
}
}
if (userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=usernameExists", true);
return new RedirectView("/addUsers?messageType=usernameExists", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/adminSettings?messageType=invalidRole", true);
return new RedirectView("/addUsers?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/adminSettings?messageType=invalidRole", true);
return new RedirectView("/addUsers?messageType=invalidRole", true);
}
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role);
} else {
if (password.isBlank()) {
return new RedirectView("/adminSettings?messageType=invalidPassword", true);
return new RedirectView("/addUsers?messageType=invalidPassword", true);
}
userService.saveUser(username, password, role, forceChange);
}
return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user
"/addUsers", // Redirect to account page after adding the user
true);
}
@ -250,32 +239,32 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) {
return new RedirectView("/adminSettings?messageType=userNotFound", true);
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=userNotFound", true);
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=downgradeCurrentUser", true);
return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
}
try {
// Validate the role
Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request
return new RedirectView("/adminSettings?messageType=invalidRole", true);
return new RedirectView("/addUsers?messageType=invalidRole", true);
}
} catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message
return new RedirectView("/adminSettings?messageType=invalidRole", true);
return new RedirectView("/addUsers?messageType=invalidRole", true);
}
User user = userOpt.get();
userService.changeRole(user, role);
return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user
"/addUsers", // Redirect to account page after adding the user
true);
}
@ -288,16 +277,16 @@ public class UserController {
throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) {
return new RedirectView("/adminSettings?messageType=userNotFound", true);
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=userNotFound", true);
return new RedirectView("/addUsers?messageType=userNotFound", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=disabledCurrentUser", true);
return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
}
User user = userOpt.get();
userService.changeUserEnabled(user, enabled);
@ -325,7 +314,7 @@ public class UserController {
}
}
return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user
"/addUsers", // Redirect to account page after adding the user
true);
}
@ -334,23 +323,23 @@ public class UserController {
public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=deleteUsernameExists", true);
return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
}
// Get the currently authenticated username
String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=deleteCurrentUser", true);
return new RedirectView("/addUsers?messageType=deleteCurrentUser", true);
}
// Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(username, false);
sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId());
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
}
userService.deleteUser(username);
return new RedirectView("/adminSettings", true);
return new RedirectView("/addUsers", true);
}
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")

View File

@ -15,7 +15,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -24,7 +24,7 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
private final ApplicationProperties applicationProperties;
@ -32,7 +32,7 @@ public class ConvertHtmlToPDF {
@Autowired
public ConvertHtmlToPDF(
CustomPDFDocumentFactory pdfDocumentFactory,
CustomPDDocumentFactory pdfDocumentFactory,
ApplicationProperties applicationProperties,
RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory;
@ -45,8 +45,7 @@ public class ConvertHtmlToPDF {
@Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description =
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format."
+ " Input:HTML Output:PDF Type:SISO")
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format. Input:HTML Output:PDF Type:SISO")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
throws Exception {
MultipartFile fileInput = request.getFileInput();

View File

@ -8,10 +8,12 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.ImageType;
@ -32,7 +34,7 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.*;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -42,10 +44,10 @@ import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertImgPDFController(CustomPDFDocumentFactory pdfDocumentFactory) {
public ConvertImgPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -53,9 +55,7 @@ public class ConvertImgPDFController {
@Operation(
summary = "Convert PDF to image(s)",
description =
"This endpoint converts a PDF file to image(s) with the specified image format,"
+ " color type, and DPI. Users can choose to get a single image or multiple"
+ " images. Input:PDF Output:Image Type:SI-Conditional")
"This endpoint converts a PDF file to image(s) with the specified image format, color type, and DPI. Users can choose to get a single image or multiple images. Input:PDF Output:Image Type:SI-Conditional")
public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws NumberFormatException, Exception {
MultipartFile file = request.getFileInput();
@ -75,7 +75,7 @@ public class ConvertImgPDFController {
;
try {
// Load the input PDF
byte[] newPdfBytes = rearrangePdfPages(file, pageOrderArr);
byte[] newPdfBytes = rearrangePdfPages(file.getBytes(), pageOrderArr);
ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) {
@ -91,7 +91,6 @@ public class ConvertImgPDFController {
result =
PdfUtils.convertFromPdf(
pdfDocumentFactory,
newPdfBytes,
"webp".equalsIgnoreCase(imageFormat)
? "png"
@ -145,7 +144,7 @@ public class ConvertImgPDFController {
List<Path> webpFiles =
Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp"))
.toList();
.collect(Collectors.toList());
if (webpFiles.isEmpty()) {
log.error("No WebP files were created in: {}", tempOutputDir.toString());
@ -209,9 +208,7 @@ public class ConvertImgPDFController {
@Operation(
summary = "Convert images to a PDF file",
description =
"This endpoint converts one or more images to a PDF file. Users can specify"
+ " whether to stretch the images to fit the PDF page, and whether to"
+ " automatically rotate the images. Input:Image Output:PDF Type:MISO")
"This endpoint converts one or more images to a PDF file. Users can specify whether to stretch the images to fit the PDF page, and whether to automatically rotate the images. Input:Image Output:PDF Type:MISO")
public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
throws IOException {
MultipartFile[] file = request.getFileInput();
@ -246,10 +243,9 @@ public class ConvertImgPDFController {
* @return A byte array of the rearranged PDF.
* @throws IOException If an error occurs while processing the PDF.
*/
private byte[] rearrangePdfPages(MultipartFile pdfFile, String[] pageOrderArr)
throws IOException {
private byte[] rearrangePdfPages(byte[] pdfBytes, String[] pageOrderArr) throws IOException {
// Load the input PDF
PDDocument document = pdfDocumentFactory.load(pdfFile);
PDDocument document = Loader.loadPDF(pdfBytes);
int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false);

View File

@ -25,7 +25,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,14 +34,14 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
private final ApplicationProperties applicationProperties;
private final RuntimePathConfig runtimePathConfig;
@Autowired
public ConvertMarkdownToPdf(
CustomPDFDocumentFactory pdfDocumentFactory,
CustomPDDocumentFactory pdfDocumentFactory,
ApplicationProperties applicationProperties,
RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory;
@ -54,8 +54,7 @@ public class ConvertMarkdownToPdf {
@Operation(
summary = "Convert a Markdown file to PDF",
description =
"This endpoint takes a Markdown file input, converts it to HTML, and then to"
+ " PDF format. Input:MARKDOWN Output:PDF Type:SISO")
"This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format. Input:MARKDOWN Output:PDF Type:SISO")
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile fileInput = request.getFileInput();

View File

@ -24,7 +24,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.api.GeneralFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,12 +34,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertOfficeController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig;
@Autowired
public ConvertOfficeController(
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig;
}
@ -93,8 +93,7 @@ public class ConvertOfficeController {
@Operation(
summary = "Convert a file to a PDF using LibreOffice",
description =
"This endpoint converts a given file to a PDF using LibreOffice API Input:ANY"
+ " Output:PDF Type:SISO")
"This endpoint converts a given file to a PDF using LibreOffice API Input:ANY Output:PDF Type:SISO")
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();

View File

@ -2,9 +2,9 @@ package stirling.software.SPDF.controller.api.converters;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
@ -21,7 +21,6 @@ import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.model.api.converters.PdfToPresentationRequest;
import stirling.software.SPDF.model.api.converters.PdfToTextOrRTFRequest;
import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.PDFToFile;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -30,19 +29,11 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Convert", description = "Convert APIs")
public class ConvertPDFToOffice {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public ConvertPDFToOffice(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/pdf/presentation")
@Operation(
summary = "Convert PDF to Presentation format",
description =
"This endpoint converts a given PDF file to a Presentation format. Input:PDF"
+ " Output:PPT Type:SISO")
"This endpoint converts a given PDF file to a Presentation format. Input:PDF Output:PPT Type:SISO")
public ResponseEntity<byte[]> processPdfToPresentation(
@ModelAttribute PdfToPresentationRequest request)
throws IOException, InterruptedException {
@ -56,15 +47,14 @@ public class ConvertPDFToOffice {
@Operation(
summary = "Convert PDF to Text or RTF format",
description =
"This endpoint converts a given PDF file to Text or RTF format. Input:PDF"
+ " Output:TXT Type:SISO")
"This endpoint converts a given PDF file to Text or RTF format. Input:PDF Output:TXT Type:SISO")
public ResponseEntity<byte[]> processPdfToRTForTXT(
@ModelAttribute PdfToTextOrRTFRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
String outputFormat = request.getOutputFormat();
if ("txt".equals(request.getOutputFormat())) {
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
PDFTextStripper stripper = new PDFTextStripper();
String text = stripper.getText(document);
return WebResponseUtils.bytesToWebResponse(
@ -84,8 +74,7 @@ public class ConvertPDFToOffice {
@Operation(
summary = "Convert PDF to Word document",
description =
"This endpoint converts a given PDF file to a Word document format. Input:PDF"
+ " Output:WORD Type:SISO")
"This endpoint converts a given PDF file to a Word document format. Input:PDF Output:WORD Type:SISO")
public ResponseEntity<byte[]> processPdfToWord(@ModelAttribute PdfToWordRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
@ -98,8 +87,7 @@ public class ConvertPDFToOffice {
@Operation(
summary = "Convert PDF to XML",
description =
"This endpoint converts a PDF file to an XML file. Input:PDF Output:XML"
+ " Type:SISO")
"This endpoint converts a PDF file to an XML file. Input:PDF Output:XML Type:SISO")
public ResponseEntity<byte[]> processPdfToXML(@ModelAttribute PDFFile request)
throws Exception {
MultipartFile inputFile = request.getFileInput();

View File

@ -20,9 +20,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -34,33 +33,25 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert")
public class ConvertWebsiteToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig;
private final ApplicationProperties applicationProperties;
@Autowired
public ConvertWebsiteToPDF(
CustomPDFDocumentFactory pdfDocumentFactory,
RuntimePathConfig runtimePathConfig,
ApplicationProperties applicationProperties) {
CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig;
this.applicationProperties = applicationProperties;
}
@PostMapping(consumes = "multipart/form-data", value = "/url/pdf")
@Operation(
summary = "Convert a URL to a PDF",
description =
"This endpoint fetches content from a URL and converts it to a PDF format."
+ " Input:N/A Output:PDF Type:SISO")
"This endpoint fetches content from a URL and converts it to a PDF format. Input:N/A Output:PDF Type:SISO")
public ResponseEntity<byte[]> urlToPdf(@ModelAttribute UrlToPdfRequest request)
throws IOException, InterruptedException {
String URL = request.getUrlInput();
if (!applicationProperties.getSystem().getEnableUrlToPDF()) {
throw new IllegalArgumentException("This endpoint has been disabled by the admin.");
}
// Validate the URL format
if (!URL.matches("^https?://.*") || !GeneralUtils.isValidURL(URL)) {
throw new IllegalArgumentException("Invalid URL format provided.");
@ -81,7 +72,6 @@ public class ConvertWebsiteToPDF {
List<String> command = new ArrayList<>();
command.add(runtimePathConfig.getWeasyPrintPath());
command.add(URL);
command.add("--pdf-forms");
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode =

View File

@ -12,8 +12,8 @@ import java.util.zip.ZipOutputStream;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.QuoteMode;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -30,7 +30,6 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.pdf.FlexibleCSVWriter;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import technology.tabula.ObjectExtractor;
import technology.tabula.Page;
@ -43,24 +42,16 @@ import technology.tabula.extractors.SpreadsheetExtractionAlgorithm;
@Slf4j
public class ExtractCSVController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public ExtractCSVController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(value = "/pdf/csv", consumes = "multipart/form-data")
@Operation(
summary = "Extracts a CSV document from a PDF",
description =
"This operation takes an input PDF file and returns CSV file of whole page."
+ " Input:PDF Output:CSV Type:SISO")
"This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO")
public ResponseEntity<?> pdfToCsv(@ModelAttribute PDFWithPageNums form) throws Exception {
String baseName = getBaseName(form.getFileInput().getOriginalFilename());
List<CsvEntry> csvEntries = new ArrayList<>();
try (PDDocument document = pdfDocumentFactory.load(form)) {
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
List<Integer> pages = form.getPageNumbersList(document, true);
SpreadsheetExtractionAlgorithm sea = new SpreadsheetExtractionAlgorithm();
CSVFormat format =

View File

@ -2,10 +2,10 @@ package stirling.software.SPDF.controller.api.filters;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -23,7 +23,6 @@ import stirling.software.SPDF.model.api.filter.ContainsTextRequest;
import stirling.software.SPDF.model.api.filter.FileSizeRequest;
import stirling.software.SPDF.model.api.filter.PageRotationRequest;
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -32,13 +31,6 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Filter", description = "Filter APIs")
public class FilterController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public FilterController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/filter-contains-text")
@Operation(
summary = "Checks if a PDF contains set text, returns true if does",
@ -49,7 +41,7 @@ public class FilterController {
String text = request.getText();
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = pdfDocumentFactory.load(inputFile);
PDDocument pdfDocument = Loader.loadPDF(inputFile.getBytes());
if (PdfUtils.hasText(pdfDocument, pageNumber, text))
return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
@ -66,7 +58,7 @@ public class FilterController {
MultipartFile inputFile = request.getFileInput();
String pageNumber = request.getPageNumbers();
PDDocument pdfDocument = pdfDocumentFactory.load(inputFile);
PDDocument pdfDocument = Loader.loadPDF(inputFile.getBytes());
if (PdfUtils.hasImages(pdfDocument, pageNumber))
return WebResponseUtils.pdfDocToWebResponse(
pdfDocument, Filenames.toSimpleFileName(inputFile.getOriginalFilename()));
@ -83,7 +75,7 @@ public class FilterController {
String pageCount = request.getPageCount();
String comparator = request.getComparator();
// Load the PDF
PDDocument document = pdfDocumentFactory.load(inputFile);
PDDocument document = Loader.loadPDF(inputFile.getBytes());
int actualPageCount = document.getNumberOfPages();
boolean valid = false;
@ -117,7 +109,7 @@ public class FilterController {
String comparator = request.getComparator();
// Load the PDF
PDDocument document = pdfDocumentFactory.load(inputFile);
PDDocument document = Loader.loadPDF(inputFile.getBytes());
PDPage firstPage = document.getPage(0);
PDRectangle actualPageSize = firstPage.getMediaBox();
@ -193,7 +185,7 @@ public class FilterController {
String comparator = request.getComparator();
// Load the PDF
PDDocument document = pdfDocumentFactory.load(inputFile);
PDDocument document = Loader.loadPDF(inputFile.getBytes());
// Get the rotation of the first page
PDPage firstPage = document.getPage(0);

View File

@ -5,10 +5,10 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -23,7 +23,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -35,25 +34,17 @@ public class AutoRenameController {
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
private static final int LINE_LIMIT = 200;
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public AutoRenameController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation(
summary = "Extract header from PDF file",
description =
"This endpoint accepts a PDF file and attempts to extract its title or header"
+ " based on heuristics. Input:PDF Output:PDF Type:SISO")
"This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
throws Exception {
MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = pdfDocumentFactory.load(file);
PDDocument document = Loader.loadPDF(file.getBytes());
PDFTextStripper reader =
new PDFTextStripper() {
List<LineInfo> lineInfos = new ArrayList<>();

View File

@ -35,7 +35,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -51,10 +51,10 @@ public class AutoSplitPdfController {
"https://github.com/Frooodle/Stirling-PDF",
"https://stirlingpdf.com"));
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public AutoSplitPdfController(CustomPDFDocumentFactory pdfDocumentFactory) {
public AutoSplitPdfController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}

View File

@ -8,6 +8,7 @@ import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageTree;
@ -30,7 +31,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.PdfUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -40,10 +41,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class BlankPageController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public BlankPageController(CustomPDFDocumentFactory pdfDocumentFactory) {
public BlankPageController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -77,16 +78,14 @@ public class BlankPageController {
@Operation(
summary = "Remove blank pages from a PDF file",
description =
"This endpoint removes blank pages from a given PDF file. Users can specify the"
+ " threshold and white percentage to tune the detection of blank pages."
+ " Input:PDF Output:PDF Type:SISO")
"This endpoint removes blank pages from a given PDF file. Users can specify the threshold and white percentage to tune the detection of blank pages. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> removeBlankPages(@ModelAttribute RemoveBlankPagesRequest request)
throws IOException, InterruptedException {
MultipartFile inputFile = request.getFileInput();
int threshold = request.getThreshold();
float whitePercent = request.getWhitePercent();
try (PDDocument document = pdfDocumentFactory.load(inputFile)) {
try (PDDocument document = Loader.loadPDF(inputFile.getBytes())) {
PDPageTree pages = document.getDocumentCatalog().getPages();
PDFTextStripper textStripper = new PDFTextStripper();

View File

@ -3,35 +3,22 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.plugins.jpeg.JPEGImageWriteParam;
import javax.imageio.stream.ImageOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.PDXObject;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@ -43,15 +30,10 @@ import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -63,588 +45,73 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class CompressController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final boolean qpdfEnabled;
private final CustomPDDocumentFactory pdfDocumentFactory;
public CompressController(
CustomPDFDocumentFactory pdfDocumentFactory,
EndpointConfiguration endpointConfiguration) {
@Autowired
public CompressController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
this.qpdfEnabled = endpointConfiguration.isGroupEnabled("qpdf");
}
@Data
@AllArgsConstructor
@NoArgsConstructor
private static class ImageReference {
int pageNum; // Page number where the image appears
COSName name; // The name used to reference this image
}
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
@NoArgsConstructor
private static class NestedImageReference extends ImageReference {
COSName formName; // Name of the form XObject containing the image
COSName imageName; // Name of the image within the form
}
// Tracks compression stats for reporting
private static class CompressionStats {
int totalImages = 0;
int nestedImages = 0;
int uniqueImagesCount = 0;
int compressedImages = 0;
int skippedImages = 0;
long totalOriginalBytes = 0;
long totalCompressedBytes = 0;
}
public Path compressImagesInPDF(
Path pdfFile, double scaleFactor, float jpegQuality, boolean convertToGrayscale)
private void compressImagesInPDF(Path pdfFile, double initialScaleFactor, boolean grayScale)
throws Exception {
Path newCompressedPDF = Files.createTempFile("compressedPDF", ".pdf");
long originalFileSize = Files.size(pdfFile);
log.info(
"Starting image compression with scale factor: {}, JPEG quality: {}, grayscale: {} on file size: {}",
scaleFactor,
jpegQuality,
convertToGrayscale,
GeneralUtils.formatBytes(originalFileSize));
byte[] fileBytes = Files.readAllBytes(pdfFile);
try (PDDocument doc = Loader.loadPDF(fileBytes)) {
double scaleFactor = initialScaleFactor;
try (PDDocument doc = pdfDocumentFactory.load(pdfFile)) {
// Find all unique images in the document
Map<String, List<ImageReference>> uniqueImages = findImages(doc);
// Get statistics
CompressionStats stats = new CompressionStats();
stats.uniqueImagesCount = uniqueImages.size();
calculateImageStats(uniqueImages, stats);
// Create compressed versions of unique images
Map<String, PDImageXObject> compressedVersions =
createCompressedImages(
doc, uniqueImages, scaleFactor, jpegQuality, convertToGrayscale, stats);
// Replace all instances with compressed versions
replaceImages(doc, uniqueImages, compressedVersions, stats);
// Log compression statistics
logCompressionStats(stats, originalFileSize);
// Free memory before saving
compressedVersions.clear();
uniqueImages.clear();
log.info("Saving compressed PDF to {}", newCompressedPDF.toString());
doc.save(newCompressedPDF.toString());
// Log overall file size reduction
long compressedFileSize = Files.size(newCompressedPDF);
double overallReduction = 100.0 - ((compressedFileSize * 100.0) / originalFileSize);
log.info(
"Overall PDF compression: {} → {} (reduced by {}%)",
GeneralUtils.formatBytes(originalFileSize),
GeneralUtils.formatBytes(compressedFileSize),
String.format("%.1f", overallReduction));
return newCompressedPDF;
}
}
// Find all images in the document, both direct and nested within forms
private Map<String, List<ImageReference>> findImages(PDDocument doc) throws IOException {
Map<String, List<ImageReference>> uniqueImages = new HashMap<>();
// Scan through all pages in the document
for (int pageNum = 0; pageNum < doc.getNumberOfPages(); pageNum++) {
PDPage page = doc.getPage(pageNum);
for (PDPage page : doc.getPages()) {
PDResources res = page.getResources();
if (res == null || res.getXObjectNames() == null) continue;
// Process all XObjects on the page
if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
// Direct image
if (isImage(xobj)) {
addDirectImage(pageNum, name, (PDImageXObject) xobj, uniqueImages);
log.info(
"Found direct image '{}' on page {} - {}x{}",
name.getName(),
pageNum + 1,
((PDImageXObject) xobj).getWidth(),
((PDImageXObject) xobj).getHeight());
}
// Form XObject that may contain nested images
else if (isForm(xobj)) {
checkFormForImages(pageNum, name, (PDFormXObject) xobj, uniqueImages);
}
}
}
return uniqueImages;
}
private boolean isImage(PDXObject xobj) {
return xobj instanceof PDImageXObject;
}
private boolean isForm(PDXObject xobj) {
return xobj instanceof PDFormXObject;
}
private ImageReference addDirectImage(
int pageNum,
COSName name,
PDImageXObject image,
Map<String, List<ImageReference>> uniqueImages)
throws IOException {
ImageReference ref = new ImageReference();
ref.pageNum = pageNum;
ref.name = name;
String imageHash = generateImageHash(image);
uniqueImages.computeIfAbsent(imageHash, k -> new ArrayList<>()).add(ref);
return ref;
}
// Look for images inside form XObjects
private void checkFormForImages(
int pageNum,
COSName formName,
PDFormXObject formXObj,
Map<String, List<ImageReference>> uniqueImages)
throws IOException {
PDResources formResources = formXObj.getResources();
if (formResources == null || formResources.getXObjectNames() == null) {
return;
}
log.info(
"Checking form XObject '{}' on page {} for nested images",
formName.getName(),
pageNum + 1);
// Process all XObjects within the form
for (COSName nestedName : formResources.getXObjectNames()) {
PDXObject nestedXobj = formResources.getXObject(nestedName);
if (isImage(nestedXobj)) {
PDImageXObject nestedImage = (PDImageXObject) nestedXobj;
log.info(
"Found nested image '{}' in form '{}' on page {} - {}x{}",
nestedName.getName(),
formName.getName(),
pageNum + 1,
nestedImage.getWidth(),
nestedImage.getHeight());
// Create specialized reference for the nested image
NestedImageReference nestedRef = new NestedImageReference();
nestedRef.pageNum = pageNum;
nestedRef.formName = formName;
nestedRef.imageName = nestedName;
String imageHash = generateImageHash(nestedImage);
uniqueImages.computeIfAbsent(imageHash, k -> new ArrayList<>()).add(nestedRef);
}
}
}
// Count total images and nested images
private void calculateImageStats(
Map<String, List<ImageReference>> uniqueImages, CompressionStats stats) {
for (List<ImageReference> references : uniqueImages.values()) {
for (ImageReference ref : references) {
stats.totalImages++;
if (ref instanceof NestedImageReference) {
stats.nestedImages++;
}
}
}
}
// Create compressed versions of all unique images
private Map<String, PDImageXObject> createCompressedImages(
PDDocument doc,
Map<String, List<ImageReference>> uniqueImages,
double scaleFactor,
float jpegQuality,
boolean convertToGrayscale,
CompressionStats stats)
throws IOException {
Map<String, PDImageXObject> compressedVersions = new HashMap<>();
// Process each unique image exactly once
for (Entry<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
String imageHash = entry.getKey();
List<ImageReference> references = entry.getValue();
if (references.isEmpty()) continue;
// Get the first instance of this image
PDImageXObject originalImage = getOriginalImage(doc, references.get(0));
// Track original size
int originalSize = (int) originalImage.getCOSObject().getLength();
stats.totalOriginalBytes += originalSize;
// Process this unique image
PDImageXObject compressedImage =
compressImage(
doc,
originalImage,
originalSize,
scaleFactor,
jpegQuality,
convertToGrayscale);
if (compressedImage != null) {
// Store the compressed version in our map
compressedVersions.put(imageHash, compressedImage);
stats.compressedImages++;
// Update compression stats
int compressedSize = (int) compressedImage.getCOSObject().getLength();
stats.totalCompressedBytes += compressedSize * references.size();
double reductionPercentage = 100.0 - ((compressedSize * 100.0) / originalSize);
log.info(
"Image hash {}: Compressed from {} to {} (reduced by {}%)",
imageHash,
GeneralUtils.formatBytes(originalSize),
GeneralUtils.formatBytes(compressedSize),
String.format("%.1f", reductionPercentage));
} else {
log.info("Image hash {}: Not suitable for compression, skipping", imageHash);
stats.totalCompressedBytes += originalSize * references.size();
stats.skippedImages++;
}
}
return compressedVersions;
}
// Get original image from a reference
private PDImageXObject getOriginalImage(PDDocument doc, ImageReference ref) throws IOException {
if (ref instanceof NestedImageReference) {
// Get the nested image from within a form XObject
NestedImageReference nestedRef = (NestedImageReference) ref;
PDPage page = doc.getPage(nestedRef.pageNum);
PDResources pageResources = page.getResources();
// Get the form XObject
PDFormXObject formXObj = (PDFormXObject) pageResources.getXObject(nestedRef.formName);
// Get the nested image from the form's resources
PDResources formResources = formXObj.getResources();
return (PDImageXObject) formResources.getXObject(nestedRef.imageName);
} else {
// Get direct image from page resources
PDPage page = doc.getPage(ref.pageNum);
PDResources resources = page.getResources();
return (PDImageXObject) resources.getXObject(ref.name);
}
}
// Try to compress an image if it makes sense
private PDImageXObject compressImage(
PDDocument doc,
PDImageXObject originalImage,
int originalSize,
double scaleFactor,
float jpegQuality,
boolean convertToGrayscale)
throws IOException {
// Process and compress the image
BufferedImage processedImage =
processAndCompressImage(
originalImage, scaleFactor, jpegQuality, convertToGrayscale);
if (processedImage == null) {
return null;
}
// Convert to bytes for storage
byte[] compressedData = convertToBytes(processedImage, jpegQuality);
// Check if compression is beneficial
if (compressedData.length < originalSize || convertToGrayscale) {
// Create a compressed version
return PDImageXObject.createFromByteArray(
doc, compressedData, originalImage.getCOSObject().toString());
}
return null;
}
// Replace all instances of original images with their compressed versions
private void replaceImages(
PDDocument doc,
Map<String, List<ImageReference>> uniqueImages,
Map<String, PDImageXObject> compressedVersions,
CompressionStats stats)
throws IOException {
for (Entry<String, List<ImageReference>> entry : uniqueImages.entrySet()) {
String imageHash = entry.getKey();
List<ImageReference> references = entry.getValue();
// Skip if no compressed version exists
PDImageXObject compressedImage = compressedVersions.get(imageHash);
if (compressedImage == null) continue;
// Replace ALL instances with the compressed version
for (ImageReference ref : references) {
replaceImageReference(doc, ref, compressedImage);
}
}
}
// Replace a specific image reference with a compressed version
private void replaceImageReference(
PDDocument doc, ImageReference ref, PDImageXObject compressedImage) throws IOException {
if (ref instanceof NestedImageReference) {
// Replace nested image within form XObject
NestedImageReference nestedRef = (NestedImageReference) ref;
PDPage page = doc.getPage(nestedRef.pageNum);
PDResources pageResources = page.getResources();
// Get the form XObject
PDFormXObject formXObj = (PDFormXObject) pageResources.getXObject(nestedRef.formName);
// Replace the nested image in the form's resources
PDResources formResources = formXObj.getResources();
formResources.put(nestedRef.imageName, compressedImage);
log.info(
"Replaced nested image '{}' in form '{}' on page {} with compressed version",
nestedRef.imageName.getName(),
nestedRef.formName.getName(),
nestedRef.pageNum + 1);
} else {
// Replace direct image in page resources
PDPage page = doc.getPage(ref.pageNum);
PDResources resources = page.getResources();
resources.put(ref.name, compressedImage);
log.info("Replaced direct image on page {} with compressed version", ref.pageNum + 1);
}
}
// Log final stats about the compression
private void logCompressionStats(CompressionStats stats, long originalFileSize) {
// Calculate image reduction percentage
double overallImageReduction =
stats.totalOriginalBytes > 0
? 100.0 - ((stats.totalCompressedBytes * 100.0) / stats.totalOriginalBytes)
: 0;
int duplicatedImages = stats.totalImages - stats.uniqueImagesCount;
log.info(
"Image compression summary - Total unique: {}, Compressed: {}, Skipped: {}, Duplicates: {}, Nested: {}",
stats.uniqueImagesCount,
stats.compressedImages,
stats.skippedImages,
duplicatedImages,
stats.nestedImages);
log.info(
"Total original image size: {}, compressed: {} (reduced by {}%)",
GeneralUtils.formatBytes(stats.totalOriginalBytes),
GeneralUtils.formatBytes(stats.totalCompressedBytes),
String.format("%.1f", overallImageReduction));
}
private BufferedImage convertToGrayscale(BufferedImage image) {
BufferedImage grayImage =
new BufferedImage(
image.getWidth(), image.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g = grayImage.createGraphics();
g.drawImage(image, 0, 0, null);
g.dispose();
return grayImage;
}
// Resize and optionally convert to grayscale
private BufferedImage processAndCompressImage(
PDImageXObject image, double scaleFactor, float jpegQuality, boolean convertToGrayscale)
throws IOException {
if (xobj instanceof PDImageXObject image) {
BufferedImage bufferedImage = image.getImage();
int originalWidth = bufferedImage.getWidth();
int originalHeight = bufferedImage.getHeight();
// Minimum dimensions to preserve reasonable quality
int MIN_WIDTH = 400;
int MIN_HEIGHT = 400;
int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
log.info("Original dimensions: {}x{}", originalWidth, originalHeight);
// Skip if already small enough
if ((originalWidth <= MIN_WIDTH || originalHeight <= MIN_HEIGHT) && !convertToGrayscale) {
log.info("Skipping - below minimum dimensions threshold");
return null;
if (newWidth == 0 || newHeight == 0) {
continue;
}
// Convert to grayscale first if requested (before resizing for better quality)
if (convertToGrayscale) {
bufferedImage = convertToGrayscale(bufferedImage);
log.info("Converted image to grayscale");
}
Image scaledImage =
bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
// Adjust scale factor for very large or very small images
double adjustedScaleFactor = scaleFactor;
if (originalWidth > 3000 || originalHeight > 3000) {
// More aggressive for very large images
adjustedScaleFactor = Math.min(scaleFactor, 0.75);
log.info("Very large image, using more aggressive scale: {}", adjustedScaleFactor);
} else if (originalWidth < 1000 || originalHeight < 1000) {
// More conservative for smaller images
adjustedScaleFactor = Math.max(scaleFactor, 0.9);
log.info("Smaller image, using conservative scale: {}", adjustedScaleFactor);
}
int newWidth = (int) (originalWidth * adjustedScaleFactor);
int newHeight = (int) (originalHeight * adjustedScaleFactor);
// Ensure minimum dimensions
newWidth = Math.max(newWidth, MIN_WIDTH);
newHeight = Math.max(newHeight, MIN_HEIGHT);
// Skip if change is negligible
if ((double) newWidth / originalWidth > 0.95
&& (double) newHeight / originalHeight > 0.95
&& !convertToGrayscale) {
log.info("Change too small, skipping compression");
return null;
}
log.info(
"Resizing to {}x{} ({}% of original)",
newWidth, newHeight, Math.round((newWidth * 100.0) / originalWidth));
BufferedImage scaledImage;
if (convertToGrayscale) {
// If already grayscale, maintain the grayscale format
scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
} else {
// Otherwise use original color model
scaledImage =
BufferedImage scaledBufferedImage;
if (grayScale
|| bufferedImage.getType() == BufferedImage.TYPE_BYTE_GRAY) {
scaledBufferedImage =
new BufferedImage(
newWidth,
newHeight,
bufferedImage.getColorModel().hasAlpha()
? BufferedImage.TYPE_INT_ARGB
: BufferedImage.TYPE_INT_RGB);
}
Graphics2D g2d = scaledImage.createGraphics();
g2d.setRenderingHint(
RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(bufferedImage, 0, 0, newWidth, newHeight, null);
g2d.dispose();
return scaledImage;
}
// Convert image to byte array with quality settings
private byte[] convertToBytes(BufferedImage scaledImage, float jpegQuality) throws IOException {
String format = scaledImage.getColorModel().hasAlpha() ? "png" : "jpeg";
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
if ("jpeg".equals(format)) {
// Get the best available JPEG writer
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpeg");
ImageWriter writer = writers.next();
JPEGImageWriteParam param = (JPEGImageWriteParam) writer.getDefaultWriteParam();
// Set compression parameters
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
param.setCompressionQuality(jpegQuality);
param.setOptimizeHuffmanTables(true); // Better compression
param.setProgressiveMode(ImageWriteParam.MODE_DEFAULT); // Progressive scanning
// Write compressed image
try (ImageOutputStream ios = ImageIO.createImageOutputStream(outputStream)) {
writer.setOutput(ios);
writer.write(null, new IIOImage(scaledImage, null, null), param);
}
writer.dispose();
newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
} else {
ImageIO.write(scaledImage, format, outputStream);
scaledBufferedImage =
new BufferedImage(
newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
}
ByteArrayOutputStream compressedImageStream =
new ByteArrayOutputStream();
ImageIO.write(scaledBufferedImage, "jpeg", compressedImageStream);
byte[] imageBytes = compressedImageStream.toByteArray();
compressedImageStream.close();
return outputStream.toByteArray();
}
// Hash function to identify identical images
private String generateImageHash(PDImageXObject image) {
try {
// Create a stream for the raw stream data
try (InputStream stream = image.getCOSObject().createRawInputStream()) {
// Read up to first 8KB of data for the hash
byte[] buffer = new byte[8192];
int bytesRead = stream.read(buffer);
if (bytesRead > 0) {
byte[] dataToHash =
bytesRead == buffer.length ? buffer : Arrays.copyOf(buffer, bytesRead);
return bytesToHexString(generatMD5(dataToHash));
}
return "empty-stream";
}
} catch (Exception e) {
log.error("Error generating image hash", e);
return "fallback-" + System.identityHashCode(image);
PDImageXObject compressedImage =
PDImageXObject.createFromByteArray(
doc, imageBytes, image.getCOSObject().toString());
res.put(name, compressedImage);
}
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
private byte[] generatMD5(byte[] data) throws IOException {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
return md.digest(data); // Get the MD5 hash of the image bytes
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("MD5 algorithm not available", e);
}
}
// Scale factors for different optimization levels
private double getScaleFactorForLevel(int optimizeLevel) {
return switch (optimizeLevel) {
case 4 -> 0.9; // 90% - lite compression
case 5 -> 0.8; // 80% - lite compression
case 6 -> 0.7; // 70% - lite compression
case 7 -> 0.6; // 60% - intense compression
case 8 -> 0.5; // 50% - intense compression
case 9, 10 -> 0.4; // 40% - intense compression
default -> 1.0; // No scaling for levels 1-3
};
Path tempOutput = Files.createTempFile("output_", ".pdf");
doc.save(tempOutput.toString());
Files.move(tempOutput, pdfFile, StandardCopyOption.REPLACE_EXISTING);
}
// JPEG quality for different optimization levels
private float getJpegQualityForLevel(int optimizeLevel) {
return switch (optimizeLevel) {
case 7 -> 0.8f; // 80% quality
case 8 -> 0.6f; // 60% quality
case 9, 10 -> 0.4f; // 40% quality
default -> 0.7f; // 70% quality for levels 1-6
};
}
@PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@ -658,7 +125,7 @@ public class CompressController {
MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize();
Boolean convertToGrayscale = request.getGrayscale();
if (expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified");
}
@ -670,136 +137,40 @@ public class CompressController {
autoMode = true;
}
// Create initial input file
Path originalFile = Files.createTempFile("original_", ".pdf");
inputFile.transferTo(originalFile.toFile());
long inputFileSize = Files.size(originalFile);
Path tempInputFile = Files.createTempFile("input_", ".pdf");
inputFile.transferTo(tempInputFile.toFile());
Path currentFile = Files.createTempFile("working_", ".pdf");
Files.copy(originalFile, currentFile, StandardCopyOption.REPLACE_EXISTING);
long inputFileSize = Files.size(tempInputFile);
// Keep track of all temporary files for cleanup
List<Path> tempFiles = new ArrayList<>();
tempFiles.add(originalFile);
tempFiles.add(currentFile);
Path tempOutputFile = null;
byte[] pdfBytes;
try {
tempOutputFile = Files.createTempFile("output_", ".pdf");
if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
}
boolean sizeMet = false;
boolean imageCompressionApplied = false;
boolean qpdfCompressionApplied = false;
if (qpdfEnabled && optimizeLevel <= 3) {
optimizeLevel = 4;
}
boolean grayscaleEnabled = Boolean.TRUE.equals(request.getGrayscale());
while (!sizeMet && optimizeLevel <= 9) {
// Apply image compression for levels 4-9
if ((optimizeLevel >= 4 || Boolean.TRUE.equals(convertToGrayscale))
&& !imageCompressionApplied) {
double scaleFactor = getScaleFactorForLevel(optimizeLevel);
float jpegQuality = getJpegQualityForLevel(optimizeLevel);
// Compress images
Path compressedImageFile =
compressImagesInPDF(
currentFile,
scaleFactor,
jpegQuality,
Boolean.TRUE.equals(convertToGrayscale));
tempFiles.add(compressedImageFile);
currentFile = compressedImageFile;
imageCompressionApplied = true;
// Apply additional image compression for levels 6-9
if (optimizeLevel >= 6) {
// Calculate scale factor based on optimization level
double scaleFactor =
switch (optimizeLevel) {
case 6 -> 0.9; // 90% of original size
case 7 -> 0.8; // 80% of original size
case 8 -> 0.65; // 70% of original size
case 9 -> 0.5; // 60% of original size
default -> 1.0;
};
compressImagesInPDF(tempInputFile, scaleFactor, grayscaleEnabled);
}
// Apply QPDF compression for all levels
if (!qpdfCompressionApplied && qpdfEnabled) {
applyQpdfCompression(request, optimizeLevel, currentFile, tempFiles);
qpdfCompressionApplied = true;
} else if (!qpdfCompressionApplied) {
// If QPDF is disabled, mark as applied and log
if (!qpdfEnabled) {
log.info("Skipping QPDF compression as QPDF group is disabled");
}
qpdfCompressionApplied = true;
}
// Check if target size reached or not in auto mode
long outputFileSize = Files.size(currentFile);
if (outputFileSize <= expectedOutputSize || !autoMode) {
sizeMet = true;
} else {
int newOptimizeLevel =
incrementOptimizeLevel(
optimizeLevel, outputFileSize, expectedOutputSize);
// Check if we can't increase the level further
if (newOptimizeLevel == optimizeLevel) {
if (autoMode) {
log.info(
"Maximum optimization level reached without meeting target size.");
sizeMet = true;
}
} else {
// Reset flags for next iteration with higher optimization level
imageCompressionApplied = false;
qpdfCompressionApplied = false;
optimizeLevel = newOptimizeLevel;
}
}
}
// Use original if optimized file is somehow larger
long finalFileSize = Files.size(currentFile);
if (finalFileSize >= inputFileSize) {
log.warn(
"Optimized file is larger than the original. Using the original file instead.");
currentFile = originalFile;
}
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_Optimized.pdf";
return WebResponseUtils.pdfDocToWebResponse(
pdfDocumentFactory.load(currentFile.toFile()), outputFilename);
} finally {
// Clean up all temporary files
for (Path tempFile : tempFiles) {
try {
Files.deleteIfExists(tempFile);
} catch (IOException e) {
log.warn("Failed to delete temporary file: " + tempFile, e);
}
}
}
}
// Run QPDF compression
private void applyQpdfCompression(
OptimizePdfRequest request, int optimizeLevel, Path currentFile, List<Path> tempFiles)
throws IOException {
long preQpdfSize = Files.size(currentFile);
log.info("Pre-QPDF file size: {}", GeneralUtils.formatBytes(preQpdfSize));
// Map optimization levels to QPDF compression levels
int qpdfCompressionLevel =
optimizeLevel <= 3
? optimizeLevel * 3 // Level 1->3, 2->6, 3->9
: 9; // Max compression for levels 4-9
// Create output file for QPDF
Path qpdfOutputFile = Files.createTempFile("qpdf_output_", ".pdf");
tempFiles.add(qpdfOutputFile);
// Build QPDF command
// Run QPDF optimization
List<String> command = new ArrayList<>();
command.add("qpdf");
if (request.getNormalize()) {
@ -808,51 +179,77 @@ public class CompressController {
if (request.getLinearize()) {
command.add("--linearize");
}
command.add("--optimize-images");
command.add("--recompress-flate");
command.add("--compression-level=" + qpdfCompressionLevel);
command.add("--compression-level=" + optimizeLevel);
command.add("--compress-streams=y");
command.add("--object-streams=generate");
command.add(currentFile.toString());
command.add(qpdfOutputFile.toString());
command.add("--no-warn");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = null;
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
// Update current file to the QPDF output
Files.copy(qpdfOutputFile, currentFile, StandardCopyOption.REPLACE_EXISTING);
long postQpdfSize = Files.size(currentFile);
double qpdfReduction = 100.0 - ((postQpdfSize * 100.0) / preQpdfSize);
log.info(
"Post-QPDF file size: {} (reduced by {}%)",
GeneralUtils.formatBytes(postQpdfSize), String.format("%.1f", qpdfReduction));
} catch (Exception e) {
if (returnCode != null && returnCode.getRc() != 3) {
throw new IOException("QPDF command failed", e);
}
// If QPDF fails, keep using the current file
log.warn("QPDF compression failed, continuing with current file", e);
throw e;
}
}
// Check if file size is within expected size or not auto mode
long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize <= expectedOutputSize || !autoMode) {
sizeMet = true;
} else {
optimizeLevel =
incrementOptimizeLevel(
optimizeLevel, outputFileSize, expectedOutputSize);
if (autoMode && optimizeLevel >= 9) {
log.info("Maximum compression level reached in auto mode");
sizeMet = true;
}
}
}
// Read the optimized PDF file
pdfBytes = Files.readAllBytes(tempOutputFile);
Path finalFile = tempOutputFile;
// Check if optimized file is larger than the original
if (pdfBytes.length > inputFileSize) {
log.warn(
"Optimized file is larger than the original. Returning the original file"
+ " instead.");
finalFile = tempInputFile;
}
String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "")
+ "_Optimized.pdf";
return WebResponseUtils.pdfDocToWebResponse(
pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
} finally {
Files.deleteIfExists(tempOutputFile);
}
}
// Pick optimization level based on target size
private int determineOptimizeLevel(double sizeReductionRatio) {
if (sizeReductionRatio > 0.9) return 1;
if (sizeReductionRatio > 0.8) return 2;
if (sizeReductionRatio > 0.7) return 3;
if (sizeReductionRatio > 0.6) return 4;
if (sizeReductionRatio > 0.3) return 5;
if (sizeReductionRatio > 0.2) return 6;
if (sizeReductionRatio > 0.15) return 7;
if (sizeReductionRatio > 0.1) return 8;
if (sizeReductionRatio > 0.5) return 5;
if (sizeReductionRatio > 0.4) return 6;
if (sizeReductionRatio > 0.3) return 7;
if (sizeReductionRatio > 0.2) return 8;
return 9;
}
// Increment optimization level if we need more compression
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize;
log.info("Current compression ratio: {}", String.format("%.2f", currentRatio));

View File

@ -1,145 +0,0 @@
package stirling.software.SPDF.controller.api.misc;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import org.apache.pdfbox.cos.*;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdfwriter.compress.CompressParameters;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFFile;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@RequestMapping("/api/v1/misc")
@Slf4j
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class DecompressPdfController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public DecompressPdfController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(value = "/decompress-pdf", consumes = "multipart/form-data")
@Operation(
summary = "Decompress PDF streams",
description = "Fully decompresses all PDF streams including text content")
public ResponseEntity<byte[]> decompressPdf(@ModelAttribute PDFFile request)
throws IOException {
MultipartFile file = request.getFileInput();
try (PDDocument document = pdfDocumentFactory.load(file)) {
// Process all objects in document
processAllObjects(document);
// Save with explicit no compression
ByteArrayOutputStream baos = new ByteArrayOutputStream();
document.save(baos, CompressParameters.NO_COMPRESSION);
String outputFilename =
file.getOriginalFilename().replaceFirst("\\.(?=[^.]+$)", "_decompressed.");
return WebResponseUtils.bytesToWebResponse(
baos.toByteArray(), outputFilename, MediaType.APPLICATION_PDF);
}
}
private void processAllObjects(PDDocument document) {
Set<COSBase> processed = new HashSet<>();
COSDocument cosDoc = document.getDocument();
// Process all objects in the document
for (COSObjectKey key : cosDoc.getXrefTable().keySet()) {
COSObject obj = cosDoc.getObjectFromPool(key);
processObject(obj, processed);
}
}
private void processObject(COSBase obj, Set<COSBase> processed) {
// Skip null objects or already processed objects to avoid infinite recursion
if (obj == null || processed.contains(obj)) return;
processed.add(obj);
if (obj instanceof COSObject cosObj) {
processObject(cosObj.getObject(), processed);
} else if (obj instanceof COSDictionary dict) {
processDictionary(dict, processed);
} else if (obj instanceof COSArray array) {
processArray(array, processed);
}
}
private void processDictionary(COSDictionary dict, Set<COSBase> processed) {
// Process all dictionary entries
for (COSName key : dict.keySet()) {
processObject(dict.getDictionaryObject(key), processed);
}
// If this is a stream, decompress it
if (dict instanceof COSStream stream) {
decompressStream(stream);
}
}
private void processArray(COSArray array, Set<COSBase> processed) {
// Process all array elements
for (int i = 0; i < array.size(); i++) {
processObject(array.get(i), processed);
}
}
private void decompressStream(COSStream stream) {
try {
log.debug("Processing stream: {}", stream);
// Only remove filter information if it exists
if (stream.containsKey(COSName.FILTER)
|| stream.containsKey(COSName.DECODE_PARMS)
|| stream.containsKey(COSName.D)) {
// Read the decompressed content first
byte[] decompressedBytes;
try (COSInputStream is = stream.createInputStream()) {
decompressedBytes = IOUtils.toByteArray(is);
}
// Now remove filter information
stream.removeItem(COSName.FILTER);
stream.removeItem(COSName.DECODE_PARMS);
stream.removeItem(COSName.D);
// Write the raw content back
try (OutputStream out = stream.createRawOutputStream()) {
out.write(decompressedBytes);
}
// Set the Length to reflect the new stream size
stream.setInt(COSName.LENGTH, decompressedBytes.length);
}
} catch (IOException e) {
log.error("Error decompressing stream", e);
// Continue processing other streams even if this one fails
}
}
}

View File

@ -14,9 +14,9 @@ import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
@ -32,7 +32,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.CheckProgramInstall;
import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -46,21 +45,11 @@ public class ExtractImageScansController {
private static final String REPLACEFIRST = "[.][^.]+$";
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public ExtractImageScansController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation(
summary = "Extract image scans from an input file",
description =
"This endpoint extracts image scans from a given file based on certain"
+ " parameters. Users can specify angle threshold, tolerance, minimum area,"
+ " minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP"
+ " Type:SIMO")
"This endpoint extracts image scans from a given file based on certain parameters. Users can specify angle threshold, tolerance, minimum area, minimum contour area, and border size. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImageScans(
@RequestBody(
description = "Form data containing file and extraction parameters",
@ -98,7 +87,7 @@ public class ExtractImageScansController {
// Check if input file is a PDF
if ("pdf".equalsIgnoreCase(extension)) {
// Load PDF document
try (PDDocument document = pdfDocumentFactory.load(form.getFileInput())) {
try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true);
int pageCount = document.getNumberOfPages();

View File

@ -20,11 +20,11 @@ import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute;
@ -40,7 +40,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.ImageProcessingUtils;
import stirling.software.SPDF.utils.WebResponseUtils;
@ -50,26 +49,17 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public ExtractImagesController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation(
summary = "Extract images from a PDF file",
description =
"This endpoint extracts images from a given PDF file and returns them in a zip"
+ " file. Users can specify the output image format. Input:PDF"
+ " Output:IMAGE/ZIP Type:SIMO")
"This endpoint extracts images from a given PDF file and returns them in a zip file. Users can specify the output image format. Input:PDF Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request)
throws IOException, InterruptedException, ExecutionException {
MultipartFile file = request.getFileInput();
String format = request.getFormat();
boolean allowDuplicates = request.isAllowDuplicates();
PDDocument document = pdfDocumentFactory.load(file);
PDDocument document = Loader.loadPDF(file.getBytes());
// Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document);

View File

@ -3,6 +3,7 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.image.BufferedImage;
import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
@ -26,7 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.FlattenRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.CustomPDDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
@RestController
@ -35,10 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class FlattenController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired
public FlattenController(CustomPDFDocumentFactory pdfDocumentFactory) {
public FlattenController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@ -46,12 +47,11 @@ public class FlattenController {
@Operation(
summary = "Flatten PDF form fields or full page",
description =
"Flattening just PDF form fields or converting each page to images to make text"
+ " unselectable. Input:PDF, Output:PDF. Type:SISO")
"Flattening just PDF form fields or converting each page to images to make text unselectable. Input:PDF, Output:PDF. Type:SISO")
public ResponseEntity<byte[]> flatten(@ModelAttribute FlattenRequest request) throws Exception {
MultipartFile file = request.getFileInput();
PDDocument document = pdfDocumentFactory.load(file);
PDDocument document = Loader.loadPDF(file.getBytes());
Boolean flattenOnlyForms = request.getFlattenOnlyForms();
if (Boolean.TRUE.equals(flattenOnlyForms)) {

View File

@ -7,10 +7,10 @@ import java.util.Calendar;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
@ -23,7 +23,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.MetadataRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils;
import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
@ -33,13 +32,6 @@ import stirling.software.SPDF.utils.propertyeditor.StringToMapPropertyEditor;
@Tag(name = "Misc", description = "Miscellaneous APIs")
public class MetadataController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public MetadataController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
private String checkUndefined(String entry) {
// Check if the string is "undefined"
if ("undefined".equals(entry)) {
@ -59,9 +51,7 @@ public class MetadataController {
@Operation(
summary = "Update metadata of a PDF file",
description =
"This endpoint allows you to update the metadata of a given PDF file. You can"
+ " add, modify, or delete standard and custom metadata fields. Input:PDF"
+ " Output:PDF Type:SISO")
"This endpoint allows you to update the metadata of a given PDF file. You can add, modify, or delete standard and custom metadata fields. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> metadata(@ModelAttribute MetadataRequest request)
throws IOException {
@ -86,7 +76,7 @@ public class MetadataController {
allRequestParams = new java.util.HashMap<String, String>();
}
// Load the PDF file into a PDDocument
PDDocument document = pdfDocumentFactory.load(pdfFile, true);
PDDocument document = Loader.loadPDF(pdfFile.getBytes());
// Get the document information from the PDF
PDDocumentInformation info = document.getDocumentInformation();

Some files were not shown because too many files have changed in this diff Show More