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/build.yml'
- any-glob-to-any-file: '.github/workflows/push-docker.yml' - any-glob-to-any-file: '.github/workflows/push-docker.yml'
- any-glob-to-any-file: 'Dockerfile' - any-glob-to-any-file: 'Dockerfile'
- any-glob-to-any-file: 'Dockerfile.fat' - any-glob-to-any-file: 'Dockerfile.*'
- any-glob-to-any-file: 'Dockerfile.ultra-lite'
- any-glob-to-any-file: 'exampleYmlFiles/*.yml' - any-glob-to-any-file: 'exampleYmlFiles/*.yml'
- any-glob-to-any-file: 'scripts/download-security-jar.sh' - any-glob-to-any-file: 'scripts/download-security-jar.sh'
- any-glob-to-any-file: 'scripts/init.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: 'test.sh'
- any-glob-to-any-file: 'test2.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: Test:
- changed-files: - changed-files:
- any-glob-to-any-file: 'cucumber/**/*' - any-glob-to-any-file: 'cucumber/**/*'
- any-glob-to-any-file: 'src/test/**/*' - any-glob-to-any-file: 'src/test**/*'
- any-glob-to-any-file: 'src/testing/**/*'
- any-glob-to-any-file: '.pre-commit-config' - 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/pre_commit.yml'
- any-glob-to-any-file: '.github/workflows/scorecards.yml' - any-glob-to-any-file: '.github/workflows/scorecards.yml'

3
.github/labels.yml vendored
View File

@ -108,6 +108,3 @@
- name: "Priority: Low" - name: "Priority: Low"
color: "00FF00" color: "00FF00"
description: "Issues or pull requests with low priority" 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: labels:
- Translation - Translation
- title: Development Tools
labels:
- Devtools
- title: Other Changes - title: Other Changes
labels: labels:
- "*" - "*"

View File

@ -164,7 +164,7 @@ def update_missing_keys(reference_file, file_list, branch=""):
if current_entry["type"] == "entry": if current_entry["type"] == "entry":
if ref_entry_copy["type"] != "entry": if ref_entry_copy["type"] != "entry":
continue 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"] ref_entry_copy["value"] = current_entry["value"]
updated_properties.append(ref_entry_copy) updated_properties.append(ref_entry_copy)
write_json_file(os.path.join(branch, file_path), updated_properties) 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")) base_dir = os.path.abspath(os.path.join(os.getcwd(), "src", "main", "resources"))
for file_path in file_arr: for file_path in file_arr:
file_normpath = os.path.normpath(file_path) absolute_path = os.path.abspath(file_path)
absolute_path = os.path.abspath(file_normpath)
# Verify that file is within the expected directory # Verify that file is within the expected directory
if not absolute_path.startswith(base_dir): 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 # 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( 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 ( if (
basename_current_file == basename_reference_file basename_current_file == basename_reference_file
or ( or (
# only local windows command # only local windows command
not file_normpath.startswith( not file_path.startswith(
os.path.join("", "src", "main", "resources", "messages_") 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_") 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_") or not basename_current_file.startswith("messages_")
): ):
continue continue
@ -293,13 +292,13 @@ def check_for_differences(reference_file, file_list, branch, actor):
else: else:
report.append("2. **Test Status:** ✅ **_Passed_**") 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 has_differences = True
output = "\n".join( output = "\n".join(
[ [
f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`" f" - `{key}`: first at line {first}, duplicate at `line {duplicate}`"
for key, first, duplicate in find_duplicate_keys( 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: permissions:
contents: read contents: read
issues: write # Required for adding reactions to comments
pull-requests: read # Required for reading PR information
jobs: jobs:
check-comment: check-comment:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
issues: write
pull-requests: read pull-requests: read
issues: read
if: | if: |
github.event.issue.pull_request && github.event.issue.pull_request &&
( (
@ -36,23 +34,13 @@ jobs:
pr_number: ${{ steps.get-pr.outputs.pr_number }} pr_number: ${{ steps.get-pr.outputs.pr_number }}
pr_repository: ${{ steps.get-pr-info.outputs.repository }} pr_repository: ${{ steps.get-pr-info.outputs.repository }}
pr_ref: ${{ steps.get-pr-info.outputs.ref }} pr_ref: ${{ steps.get-pr-info.outputs.ref }}
comment_id: ${{ github.event.comment.id }}
enable_security: ${{ steps.check-security-flag.outputs.enable_security }}
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit 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 - name: Get PR data
id: get-pr id: get-pr
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
@ -85,61 +73,19 @@ jobs:
core.setOutput('repository', repository); core.setOutput('repository', repository);
core.setOutput('ref', pr.head.ref); 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: deploy-pr:
needs: check-comment needs: check-comment
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: read pull-requests: write
issues: write issues: write
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit 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 - name: Checkout PR
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
@ -148,24 +94,19 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK - name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- name: Run Gradle Command - name: Run Gradle Command
run: | run: ./gradlew clean build
if [ "${{ needs.check-comment.outputs.enable_security }}" == "true" ]; then
export DOCKER_ENABLE_SECURITY=true
else
export DOCKER_ENABLE_SECURITY=false
fi
./gradlew clean build
env: env:
DOCKER_ENABLE_SECURITY: false
STIRLING_PDF_DESKTOP_UI: false STIRLING_PDF_DESKTOP_UI: false
- name: Set up Docker Buildx - 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 - name: Get version number
id: versionNumber id: versionNumber
@ -174,19 +115,19 @@ jobs:
echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT echo "versionNumber=$VERSION" >> $GITHUB_OUTPUT
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
username: ${{ secrets.DOCKER_HUB_USERNAME }} username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }} password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image - name: Build and push PR-specific image
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 uses: docker/build-push-action@0adf9959216b96bec444f325f1e493d4aa344497 # v6.14.0
with: with:
context: . context: .
file: ./Dockerfile file: ./Dockerfile
push: true push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }} 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 platforms: linux/amd64
- name: Set up SSH - name: Set up SSH
@ -196,21 +137,9 @@ jobs:
sudo chmod 600 ../private.key sudo chmod 600 ../private.key
- name: Deploy to VPS - name: Deploy to VPS
id: deploy
run: | 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 # First create the docker-compose content locally
cat > docker-compose.yml << EOF cat > docker-compose.yml << 'EOF'
version: '3.3' version: '3.3'
services: services:
stirling-pdf: 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 }}/config:/configs:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw - /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
environment: environment:
DOCKER_ENABLE_SECURITY: "${DOCKER_SECURITY}" DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}" SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-GB SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}" UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest" 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 # 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 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 # Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs} mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
@ -251,65 +180,19 @@ jobs:
docker-compose up -d docker-compose up -d
ENDSSH 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 - name: Post deployment URL to PR
if: success() if: success()
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with: with:
github-token: ${{ steps.generate-token.outputs.token }}
script: | script: |
const { GITHUB_REPOSITORY } = process.env; const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/'); const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = ${{ needs.check-comment.outputs.pr_number }}; const prNumber = ${{ needs.check-comment.outputs.pr_number }};
const securityStatus = process.env.security_status || "Security Disabled";
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`; const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
const commentBody = `## 🚀 PR Test Deployment\n\n` + const commentBody = `## 🚀 PR Test Deployment\n\n` +
`Your PR has been deployed for testing!\n\n` + `Your PR has been deployed for testing!\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n` + `🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
`${securityStatus}\n\n` +
`This deployment will be automatically cleaned up when the PR is closed.\n\n`; `This deployment will be automatically cleaned up when the PR is closed.\n\n`;
await github.rest.issues.createComment({ await github.rest.issues.createComment({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,19 +14,19 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Harden Runner - name: Harden Runner
uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1 uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0
with: with:
egress-policy: audit egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
with: with:
java-version: "17" java-version: "17"
distribution: "temurin" distribution: "temurin"
- uses: gradle/actions/setup-gradle@06832c7b30a0129d7fb559bcc6e43d26f6374244 # v4.3.1 - uses: gradle/actions/setup-gradle@94baf225fe0a508e581a564467443d0e2379123b # v4.3.0
- name: Generate Swagger documentation - name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs run: ./gradlew generateOpenApiDocs
@ -35,7 +35,6 @@ jobs:
run: ./gradlew swaggerhubUpload run: ./gradlew swaggerhubUpload
env: env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
@ -43,7 +42,6 @@ jobs:
- name: Set API version as published and default on SwaggerHub - name: Set API version as published and default on SwaggerHub
run: | 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: env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
SWAGGERHUB_USER: "Frooodle"

View File

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

View File

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

6
.gitignore vendored
View File

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

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.6 rev: v0.8.4
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -12,7 +12,7 @@ repos:
files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$ files: ^((\.github/scripts|scripts)/.+)?[^/]+\.py$
exclude: (split_photos.py) exclude: (split_photos.py)
- repo: https://github.com/codespell-project/codespell - repo: https://github.com/codespell-project/codespell
rev: v2.4.1 rev: v2.3.0
hooks: hooks:
- id: codespell - id: codespell
args: args:
@ -22,7 +22,7 @@ repos:
files: \.(html|css|js|py|md)$ files: \.(html|css|js|py|md)$
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js) exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
- repo: https://github.com/gitleaks/gitleaks - repo: https://github.com/gitleaks/gitleaks
rev: v8.24.3 rev: v8.22.0
hooks: hooks:
- id: gitleaks - id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks - 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.black-formatter", // Python code formatter using Black
"ms-python.flake8", // Flake8 linter for Python to enforce code quality "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-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 // "ms-vscode-remote.remote-containers", // Support for remote development with containers (Docker, Dev Containers)
// "Oracle.oracle-java", // Oracle Java extension with additional features for Java development // "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 "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-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-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 "vscjava.vscode-spring-boot-dashboard", // Spring Boot dashboard for managing and visualizing Spring Boot applications
"EditorConfig.EditorConfig", // EditorConfig support for maintaining consistent coding styles "vscjava.vscode-spring-initializr" // Support for Spring Initializr to create new Spring projects
"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
] ]
} }

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.wordSegmenterLocales": "",
"editor.guides.bracketPairs": "active", "editor.guides.bracketPairs": "active",
"editor.guides.bracketPairsHorizontal": "active", "editor.guides.bracketPairsHorizontal": "active",
"cSpell.enabled": false, "editor.indentSize": "tabSize",
"[java]": { "editor.stickyScroll.enabled": false,
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code" "editor.minimap.enabled": false,
}, "editor.formatOnSave": true,
"java.compile.nullAnalysis.mode": "automatic", "editor.insertSpaces": true,
"java.configuration.updateBuildConfiguration": "interactive",
"java.format.enabled": true, "java.format.enabled": true,
"java.format.settings.profile": "GoogleStyle", "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", "java.format.settings.google.extra": "--aosp --skip-sorting-imports --skip-javadoc-formatting",
// (DE) Aktiviert Kommentare im Java-Format. // (DE) Aktiviert Kommentare im Java-Format.
// (EN) Enables comments in Java formatting. // (EN) Enables comments in Java formatting.
@ -80,4 +144,5 @@
"spring.initializr.defaultLanguage": "Java", "spring.initializr.defaultLanguage": "Java",
"spring.initializr.defaultGroupId": "stirling.software.SPDF", "spring.initializr.defaultGroupId": "stirling.software.SPDF",
"spring.initializr.defaultArtifactId": "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: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security container_name: Stirling-PDF-Security
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest image: stirlingtools/stirling-pdf:latest
deploy: deploy:
resources: resources:
limits: limits:

View File

@ -25,16 +25,20 @@ LABEL org.opencontainers.image.keywords="PDF, manipulation, merge, split, conver
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \ 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_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
JAVA_CUSTOM_OPTS="" \ -XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
PUID=1000 \ PUID=1000 \
PGID=1000 \ PGID=1000 \
UMASK=022 \ UMASK=022 \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
PATH=$PATH:/opt/venv/bin
# JDK for app # JDK for app
@ -62,10 +66,6 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
poppler-utils \ poppler-utils \
# OCR MY PDF (unpaper for descew and other advanced features) # OCR MY PDF (unpaper for descew and other advanced features)
tesseract-ocr-data-eng \ tesseract-ocr-data-eng \
tesseract-ocr-data-chi_sim \
tesseract-ocr-data-deu \
tesseract-ocr-data-fra \
tesseract-ocr-data-por \
# CV # CV
py3-opencv \ py3-opencv \
python3 \ python3 \
@ -73,8 +73,9 @@ RUN echo "@main https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/a
py3-pillow@testing \ py3-pillow@testing \
py3-pdf2image@testing && \ py3-pdf2image@testing && \
python3 -m venv /opt/venv && \ python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \ export PATH="/opt/venv/bin:$PATH" && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ 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/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/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ 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 # Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"] 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 # Build the application
FROM gradle:8.13-jdk21 AS build FROM gradle:8.12-jdk17 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
# Set the working directory # Set the working directory
WORKDIR /app WORKDIR /app
@ -16,7 +10,7 @@ COPY . .
# Build the application with DOCKER_ENABLE_SECURITY=false # Build the application with DOCKER_ENABLE_SECURITY=false
RUN DOCKER_ENABLE_SECURITY=true \ RUN DOCKER_ENABLE_SECURITY=true \
STIRLING_PDF_DESKTOP_UI=false \ STIRLING_PDF_DESKTOP_UI=false \
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube ./gradlew clean build
# Main stage # Main stage
FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c FROM alpine:3.21.3@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c
@ -32,8 +26,13 @@ ARG VERSION_TAG
# Set Environment Variables # Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \ 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_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
JAVA_CUSTOM_OPTS="" \ -XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
PUID=1000 \ PUID=1000 \
PGID=1000 \ PGID=1000 \
@ -42,8 +41,7 @@ ENV DOCKER_ENABLE_SECURITY=false \
INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \ PYTHONPATH=/usr/lib/libreoffice/program:/opt/venv/lib/python3.12/site-packages \
UNO_PATH=/usr/lib/libreoffice/program \ UNO_PATH=/usr/lib/libreoffice/program \
URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc \ URE_BOOTSTRAP=file:///usr/lib/libreoffice/program/fundamentalrc
PATH=$PATH:/opt/venv/bin
# JDK for app # 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) # OCR MY PDF (unpaper for descew and other advanced featues)
qpdf \ qpdf \
tesseract-ocr-data-eng \ 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 \ font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra font-liberation font-linux-libertine \
# CV # CV
py3-opencv \ 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-pillow@testing \
py3-pdf2image@testing && \ py3-pdf2image@testing && \
python3 -m venv /opt/venv && \ python3 -m venv /opt/venv && \
/opt/venv/bin/pip install --upgrade pip && \ export PATH="/opt/venv/bin:$PATH" && \
/opt/venv/bin/pip install --no-cache-dir --upgrade unoserver weasyprint && \ 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/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/unohelper.py /opt/venv/lib/python3.12/site-packages/ && \
ln -s /usr/lib/libreoffice/program /opt/venv/lib/python3.12/site-packages/LibreOffice && \ 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 EXPOSE 8080/tcp
# Set user and run command # Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"] 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 \ ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \ 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_TOOL_OPTIONS="-XX:+UnlockExperimentalVMOptions \
JAVA_CUSTOM_OPTS="" \ -XX:MaxRAMPercentage=75 \
-XX:InitiatingHeapOccupancyPercent=20 \
-XX:+G1PeriodicGCInvokesConcurrent \
-XX:G1PeriodicGCInterval=10000 \
-XX:+UseStringDeduplication \
-XX:G1PeriodicGCSystemLoadThreshold=70" \
PUID=1000 \ PUID=1000 \
PGID=1000 \ PGID=1000 \
UMASK=022 UMASK=022

View File

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

View File

@ -1,35 +1,31 @@
plugins { plugins {
id "java" 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 "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 "io.swagger.swaggerhub" version "1.3.2"
id "edu.sc.seis.launch4j" version "3.0.6" 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 "com.github.jk1.dependency-license-report" version "2.9"
//id "nebula.lint" version "19.0.3" //id "nebula.lint" version "19.0.3"
id("org.panteleyev.jpackageplugin") version "1.6.1" id("org.panteleyev.jpackageplugin") version "1.6.1"
id "org.sonarqube" version "6.1.0.5360" id "org.sonarqube" version "6.0.1.5171"
} }
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
import org.gradle.internal.os.OperatingSystem
import java.nio.file.Files
import java.time.Year
ext { ext {
springBootVersion = "3.4.4" springBootVersion = "3.4.3"
pdfboxVersion = "3.0.4" pdfboxVersion = "3.0.4"
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.38" lombokVersion = "1.18.36"
bouncycastleVersion = "1.80" bouncycastleVersion = "1.80"
springSecuritySamlVersion = "6.4.4" springSecuritySamlVersion = "6.4.3"
openSamlVersion = "4.3.2" openSamlVersion = "4.3.2"
tempJrePath = null
} }
group = "stirling.software" group = "stirling.software"
version = "0.45.6" version = "0.43.1"
java { java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21
@ -102,12 +98,11 @@ openApi {
apiDocsUrl = "http://localhost:8080/v1/api-docs" apiDocsUrl = "http://localhost:8080/v1/api-docs"
outputDir = file("$projectDir") outputDir = file("$projectDir")
outputFileName = "SwaggerDoc.json" outputFileName = "SwaggerDoc.json"
waitTimeInSeconds = 60 // Increase the wait time to 60 seconds
} }
//0.11.5 to 2024.11.5 //0.11.5 to 2024.11.5
static def getMacVersion(String version) { def getMacVersion(String version) {
def currentYear = Year.now().getValue() def currentYear = java.time.Year.now().getValue()
def versionParts = version.split("\\.", 2) def versionParts = version.split("\\.", 2)
return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}" return "${currentYear}.${versionParts.length > 1 ? versionParts[1] : versionParts[0]}"
} }
@ -118,7 +113,6 @@ jpackage {
mainJar = "Stirling-PDF-${project.version}.jar" mainJar = "Stirling-PDF-${project.version}.jar"
appName = "Stirling-PDF" appName = "Stirling-PDF"
appVersion = project.version appVersion = project.version
// appVersion = "2005.45.1"
vendor = "Stirling-Software" vendor = "Stirling-Software"
appDescription = "Stirling PDF - Your Local PDF Editor" appDescription = "Stirling PDF - Your Local PDF Editor"
icon = "src/main/resources/static/favicon.ico" icon = "src/main/resources/static/favicon.ico"
@ -164,21 +158,23 @@ jpackage {
appVersion = getMacVersion(project.version.toString()) appVersion = getMacVersion(project.version.toString())
icon = "src/main/resources/static/favicon.icns" icon = "src/main/resources/static/favicon.icns"
type = "dmg" type = "dmg"
macPackageIdentifier = "Stirling-PDF" macPackageIdentifier = "com.stirling.software.pdf"
macPackageName = "Stirling-PDF" macPackageName = "Stirling-PDF"
macAppCategory = "public.app-category.productivity" macAppCategory = "public.app-category.productivity"
macSign = false // Enable signing macSign = false // Enable signing
macAppStore = false // Not targeting App Store initially macAppStore = false // Not targeting App Store initially
// // Add license and other documentation to DMG //installDir = "Applications"
// /*macDmgContent = [
// "README.md", // Add license and other documentation to DMG
// "LICENSE", /*macDmgContent = [
// "CHANGELOG.md" "README.md",
// ]*/ "LICENSE",
// "CHANGELOG.md"
// // Enable Mac-specific entitlements ]*/
// //macEntitlements = "entitlements.plist" // You'll need to create this file
// Enable Mac-specific entitlements
//macEntitlements = "entitlements.plist" // You'll need to create this file
} }
// Linux-specific configuration // Linux-specific configuration
@ -224,109 +220,6 @@ jpackage {
licenseFile = "LICENSE" 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 { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
@ -363,9 +256,9 @@ launch4j {
spotless { spotless {
java { 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") importOrder("java", "javax", "org", "com", "net", "io", "jakarta", "lombok", "me", "stirling")
toggleOffOn() toggleOffOn()
@ -391,22 +284,18 @@ sonar {
// } // }
tasks.wrapper { tasks.wrapper {
gradleVersion = "8.12" gradleVersion = "8.12"
distributionType = Wrapper.DistributionType.ALL
} }
//tasks.withType(JavaCompile) { //tasks.withType(JavaCompile) {
// options.compilerArgs << "-Xlint:deprecation" // options.compilerArgs << "-Xlint:deprecation"
//} //}
configurations.all { 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" exclude group: "org.springframework.boot", module: "spring-boot-starter-tomcat"
} }
dependencies { dependencies {
//tmp for security bumps //tmp for security bumps
implementation 'ch.qos.logback:logback-core:1.5.18' implementation 'ch.qos.logback:logback-core:1.5.16'
implementation 'ch.qos.logback:logback-classic:1.5.18' implementation 'ch.qos.logback:logback-classic:1.5.16'
// Exclude vulnerable BouncyCastle version used in tableau // Exclude vulnerable BouncyCastle version used in tableau
@ -423,7 +312,7 @@ dependencies {
} }
//security updates //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") implementation("io.github.pixee:java-security-toolkit:1.2.1")
@ -437,17 +326,13 @@ dependencies {
if (System.getenv("DOCKER_ENABLE_SECURITY") != "false") { 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.springframework.boot:spring-boot-starter-security:$springBootVersion"
implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.3.RELEASE" 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-data-jpa:$springBootVersion"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-oauth2-client:$springBootVersion"
implementation "org.springframework.session:spring-session-core:3.4.2" 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' implementation 'com.unboundid.product.scim2:scim2-sdk-client:2.3.5'
// Don't upgrade h2database // Don't upgrade h2database
@ -491,18 +376,24 @@ dependencies {
// Image metadata extractor // Image metadata extractor
implementation "com.drewnoakes:metadata-extractor:2.19.0" 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" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0"
//general PDF //general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv // 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: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 // https://mvnrepository.com/artifact/technology.tabula/tabula
implementation ('technology.tabula:tabula:1.0.5') { implementation ('technology.tabula:tabula:1.0.5') {
@ -516,7 +407,7 @@ dependencies {
implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcprov-jdk18on:$bouncycastleVersion"
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion" implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastleVersion"
implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion" implementation "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
implementation "io.micrometer:micrometer-core:1.14.6" implementation "io.micrometer:micrometer-core:1.14.4"
implementation group: "com.google.zxing", name: "core", version: "3.5.3" implementation group: "com.google.zxing", name: "core", version: "3.5.3"
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation "org.commonmark:commonmark:0.24.0" implementation "org.commonmark:commonmark:0.24.0"
@ -543,28 +434,18 @@ compileJava {
} }
task writeVersion { task writeVersion {
def propsFile = file("$projectDir/src/main/resources/version.properties") def propsFile = file("src/main/resources/version.properties")
def propsDir = propsFile.parentFile def props = new Properties()
props.setProperty("version", version)
doLast { props.store(propsFile.newWriter(), null)
if (!propsDir.exists()) {
propsDir.mkdirs()
}
def props = new Properties()
props.setProperty("version", version)
props.store(propsFile.newWriter(), null)
}
} }
processResources.dependsOn(writeVersion)
swaggerhubUpload { swaggerhubUpload {
// dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs // dependsOn = generateOpenApiDocs // Depends on your task generating Swagger docs
api = "Stirling-PDF" // The name of your API on SwaggerHub 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 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 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 oas = "3.0.0" // The version of the OpenAPI Specification you"re using
} }
@ -575,12 +456,12 @@ jar {
attributes "Implementation-Title": "Stirling-PDF", attributes "Implementation-Title": "Stirling-PDF",
"Implementation-Version": project.version "Implementation-Version": project.version
} }
} }
tasks.named("test") { tasks.named("test") {
useJUnitPlatform() useJUnitPlatform()
} }
task printVersion { task printVersion {
doLast { doLast {
println project.version println project.version
@ -592,7 +473,3 @@ task printMacVersion {
println getMacVersion(project.version.toString()) 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: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security-Fat-Postgres 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: deploy:
resources: resources:
limits: limits:

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite-Security 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: deploy:
resources: resources:
limits: limits:

View File

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

View File

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

View File

@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security-Fat-with-login container_name: Stirling-PDF-Security-Fat-with-login
image: docker.stirlingpdf.com/stirlingtools/stirling-pdf:latest-fat image: stirlingtools/stirling-pdf:latest-fat
deploy: deploy:
resources: resources:
limits: 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 # Check for DOCKER_ENABLE_SECURITY and download the appropriate JAR if required
if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then if [ "$DOCKER_ENABLE_SECURITY" = "true" ] && [ "$VERSION_TAG" != "alpha" ]; then
if [ ! -f app-security.jar ]; then if [ ! -f app-security.jar ]; then
echo "Trying to download from: 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://files.stirlingpdf.com/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 if [ $? -ne 0 ]; then
echo "Trying to download from: 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://files.stirlingpdf.com/$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 fi
if [ $? -eq 0 ]; then # checks if curl was successful if [ $? -eq 0 ]; then # checks if curl was successful

View File

@ -1,8 +1,5 @@
#!/bin/bash #!/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 # Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
import stirling.software.SPDF.model.ApplicationProperties; 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 @Configuration
@Order(Ordered.HIGHEST_PRECEDENCE) @Order(Ordered.HIGHEST_PRECEDENCE)
@ -26,100 +22,15 @@ public class EEAppConfig {
ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) { ApplicationProperties applicationProperties, LicenseKeyChecker licenseKeyChecker) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.licenseKeyChecker = licenseKeyChecker; this.licenseKeyChecker = licenseKeyChecker;
migrateEnterpriseSettingsToPremium(this.applicationProperties);
}
@Bean(name = "runningProOrHigher")
public boolean runningProOrHigher() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() != License.NORMAL;
} }
@Bean(name = "runningEE") @Bean(name = "runningEE")
public boolean runningEnterprise() { public boolean runningEnterpriseEdition() {
return licenseKeyChecker.getPremiumLicenseEnabledResult() == License.ENTERPRISE; return licenseKeyChecker.getEnterpriseEnabledResult();
} }
@Bean(name = "SSOAutoLogin") @Bean(name = "SSOAutoLogin")
public boolean ssoAutoLogin() { public boolean ssoAutoLogin() {
return applicationProperties.getPremium().getProFeatures().isSsoAutoLogin(); return applicationProperties.getEnterpriseEdition().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());
}
}
} }
} }

View File

@ -4,17 +4,12 @@ import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.posthog.java.shaded.org.json.JSONException;
import com.posthog.java.shaded.org.json.JSONObject; import com.posthog.java.shaded.org.json.JSONObject;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -25,26 +20,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Service @Service
@Slf4j @Slf4j
public class KeygenLicenseVerifier { public class KeygenLicenseVerifier {
// todo: place in config files?
enum License {
NORMAL,
PRO,
ENTERPRISE
}
// License verification configuration
private static final String ACCOUNT_ID = "e5430f69-e834-4ae4-befd-b602aae5f372"; 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 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 static final ObjectMapper objectMapper = new ObjectMapper();
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@Autowired @Autowired
@ -52,378 +32,9 @@ public class KeygenLicenseVerifier {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
} }
public License verifyLicense(String licenseKeyOrCert) { public boolean verifyLicense(String licenseKey) {
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) {
try { try {
String encodedPayload = licenseFile; log.info("Checking license key");
// 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");
String machineFingerprint = generateMachineFingerprint(); String machineFingerprint = generateMachineFingerprint();
// First, try to validate the license // First, try to validate the license
@ -433,7 +44,7 @@ public class KeygenLicenseVerifier {
String licenseId = validationResponse.path("data").path("id").asText(); String licenseId = validationResponse.path("data").path("id").asText();
if (!isValid) { if (!isValid) {
String code = validationResponse.path("meta").path("code").asText(); String code = validationResponse.path("meta").path("code").asText();
log.info(code); log.debug(code);
if ("NO_MACHINE".equals(code) if ("NO_MACHINE".equals(code)
|| "NO_MACHINES".equals(code) || "NO_MACHINES".equals(code)
|| "FINGERPRINT_SCOPE_MISMATCH".equals(code)) { || "FINGERPRINT_SCOPE_MISMATCH".equals(code)) {
@ -458,7 +69,7 @@ public class KeygenLicenseVerifier {
return false; return false;
} catch (Exception e) { } catch (Exception e) {
log.error("Error verifying standard license: {}", e.getMessage()); log.error("Error verifying license: {}", e.getMessage());
return false; return false;
} }
} }
@ -485,7 +96,7 @@ public class KeygenLicenseVerifier {
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); 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()); JsonNode jsonResponse = objectMapper.readTree(response.body());
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
JsonNode metaNode = jsonResponse.path("meta"); JsonNode metaNode = jsonResponse.path("meta");
@ -494,11 +105,10 @@ public class KeygenLicenseVerifier {
String detail = metaNode.path("detail").asText(); String detail = metaNode.path("detail").asText();
String code = metaNode.path("code").asText(); String code = metaNode.path("code").asText();
log.info("License validity: " + isValid); log.debug("License validity: " + isValid);
log.info("Validation detail: " + detail); log.debug("Validation detail: " + detail);
log.info("Validation code: " + code); log.debug("Validation code: " + code);
// Extract user count
int users = int users =
jsonResponse jsonResponse
.path("data") .path("data")
@ -506,17 +116,7 @@ public class KeygenLicenseVerifier {
.path("metadata") .path("metadata")
.path("users") .path("users")
.asInt(0); .asInt(0);
applicationProperties.getPremium().setMaxUsers(users); applicationProperties.getEnterpriseEdition().setMaxUsers(users);
// Extract isEnterprise flag
isEnterpriseLicense =
jsonResponse
.path("data")
.path("attributes")
.path("metadata")
.path("isEnterprise")
.asBoolean(false);
log.info(applicationProperties.toString()); log.info(applicationProperties.toString());
} else { } else {
@ -548,8 +148,13 @@ public class KeygenLicenseVerifier {
.put("fingerprint", machineFingerprint) .put("fingerprint", machineFingerprint)
.put( .put(
"platform", "platform",
System.getProperty("os.name")) System.getProperty(
.put("name", hostname)) "os.name")) // Added
// platform
// parameter
.put(
"name",
hostname)) // Added name parameter
.put( .put(
"relationships", "relationships",
new JSONObject() new JSONObject()
@ -571,12 +176,16 @@ public class KeygenLicenseVerifier {
.uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines")) .uri(URI.create(BASE_URL + "/" + ACCOUNT_ID + "/machines"))
.header("Content-Type", "application/vnd.api+json") .header("Content-Type", "application/vnd.api+json")
.header("Accept", "application/vnd.api+json") .header("Accept", "application/vnd.api+json")
.header("Authorization", "License " + licenseKey) .header(
.POST(HttpRequest.BodyPublishers.ofString(body.toString())) "Authorization",
"License " + licenseKey) // Keep the license key authentication
.POST(
HttpRequest.BodyPublishers.ofString(
body.toString())) // Send the JSON body
.build(); .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); 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) { if (response.statusCode() == 201) {
log.info("Machine activated successfully"); log.info("Machine activated successfully");
return true; return true;

View File

@ -1,9 +1,6 @@
package stirling.software.SPDF.EE; package stirling.software.SPDF.EE;
import java.io.IOException; 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.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@ -11,7 +8,6 @@ import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.EE.KeygenLicenseVerifier.License;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@ -19,13 +15,11 @@ import stirling.software.SPDF.utils.GeneralUtils;
@Slf4j @Slf4j
public class LicenseKeyChecker { public class LicenseKeyChecker {
private static final String FILE_PREFIX = "file:";
private final KeygenLicenseVerifier licenseService; private final KeygenLicenseVerifier licenseService;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private License premiumEnabledResult = License.NORMAL; private boolean enterpriseEnabledResult = false;
@Autowired @Autowired
public LicenseKeyChecker( public LicenseKeyChecker(
@ -41,60 +35,27 @@ public class LicenseKeyChecker {
} }
private void checkLicense() { private void checkLicense() {
if (!applicationProperties.getPremium().isEnabled()) { if (!applicationProperties.getEnterpriseEdition().isEnabled()) {
premiumEnabledResult = License.NORMAL; enterpriseEnabledResult = false;
} else { } else {
String licenseKey = getLicenseKeyContent(applicationProperties.getPremium().getKey()); enterpriseEnabledResult =
if (licenseKey != null) { licenseService.verifyLicense(
premiumEnabledResult = licenseService.verifyLicense(licenseKey); applicationProperties.getEnterpriseEdition().getKey());
if (License.ENTERPRISE == premiumEnabledResult) { if (enterpriseEnabledResult) {
log.info("License key is Enterprise."); log.info("License key is valid.");
} else if (License.PRO == premiumEnabledResult) {
log.info("License key is Pro.");
} else {
log.info("License key is invalid, defaulting to non pro license.");
}
} else { } else {
log.error("Failed to obtain license key content."); log.info("License key is invalid.");
premiumEnabledResult = License.NORMAL;
} }
} }
} }
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 { public void updateLicenseKey(String newKey) throws IOException {
applicationProperties.getPremium().setKey(newKey); applicationProperties.getEnterpriseEdition().setKey(newKey);
GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey); GeneralUtils.saveKeyToSettings("EnterpriseEdition.key", newKey);
checkLicense(); checkLicense();
} }
public License getPremiumLicenseEnabledResult() { public boolean getEnterpriseEnabledResult() {
return premiumEnabledResult; return enterpriseEnabledResult;
} }
} }

View File

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

View File

@ -93,21 +93,8 @@ public class DesktopBrowser implements WebBrowser {
setupMainFrame(); setupMainFrame();
setupLoadHandler(); setupLoadHandler();
// Force initialize UI after 7 seconds if not already done // Show the frame immediately but transparent
Timer timeoutTimer = frame.setVisible(true);
new Timer(
2500,
e -> {
log.warn(
"Loading timeout reached. Forcing"
+ " UI transition.");
if (!browserInitialized) {
// Force UI initialization
forceInitializeUI();
}
});
timeoutTimer.setRepeats(false);
timeoutTimer.start();
}); });
} catch (Exception e) { } catch (Exception e) {
log.error("Error initializing JCEF browser: ", e); log.error("Error initializing JCEF browser: ", e);
@ -251,8 +238,8 @@ public class DesktopBrowser implements WebBrowser {
boolean canGoBack, boolean canGoBack,
boolean canGoForward) { boolean canGoForward) {
log.debug( log.debug(
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:" "Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
+ " {}, browserInitialized: {}, Time elapsed: {}ms", + "browserInitialized: {}, Time elapsed: {}ms",
isLoading, isLoading,
canGoBack, canGoBack,
canGoForward, canGoForward,
@ -261,8 +248,7 @@ public class DesktopBrowser implements WebBrowser {
if (!isLoading && !browserInitialized) { if (!isLoading && !browserInitialized) {
log.info( log.info(
"Browser finished loading, preparing to initialize UI" "Browser finished loading, preparing to initialize UI components");
+ " components");
browserInitialized = true; browserInitialized = true;
SwingUtilities.invokeLater( SwingUtilities.invokeLater(
() -> { () -> {
@ -303,12 +289,10 @@ public class DesktopBrowser implements WebBrowser {
browser.getUIComponent() browser.getUIComponent()
.requestFocus(); .requestFocus();
log.info( log.info(
"Browser component" "Browser component focused");
+ " focused");
} catch (Exception ex) { } catch (Exception ex) {
log.error( log.error(
"Error focusing" "Error focusing browser",
+ " browser",
ex); ex);
} }
}); });
@ -431,67 +415,4 @@ public class DesktopBrowser implements WebBrowser {
if (cefApp != null) cefApp.dispose(); if (cefApp != null) cefApp.dispose();
if (loadingWindow != null) loadingWindow.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.Properties;
import java.util.function.Predicate; 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.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -20,8 +19,6 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.spring6.SpringTemplateEngine; import org.thymeleaf.spring6.SpringTemplateEngine;
import com.posthog.java.shaded.kotlin.text.Regex;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@ -81,11 +78,6 @@ public class AppConfig {
return applicationProperties.getUi().getLanguages(); return applicationProperties.getUi().getLanguages();
} }
@Bean
public String contextPath(@Value("${server.servlet.context-path}") String contextPath) {
return contextPath;
}
@Bean(name = "navBarText") @Bean(name = "navBarText")
public String navBarText() { public String navBarText() {
String defaultNavBar = String defaultNavBar =
@ -184,7 +176,7 @@ public class AppConfig {
@Bean(name = "analyticsEnabled") @Bean(name = "analyticsEnabled")
@Scope("request") @Scope("request")
public boolean analyticsEnabled() { public boolean analyticsEnabled() {
if (applicationProperties.getPremium().isEnabled()) return true; if (applicationProperties.getEnterpriseEdition().isEnabled()) return true;
return applicationProperties.getSystem().isAnalyticsEnabled(); return applicationProperties.getSystem().isAnalyticsEnabled();
} }

View File

@ -9,7 +9,6 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -57,8 +56,6 @@ public class ConfigInitializer {
YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath); YamlHelper settingsTemplateFile = new YamlHelper(tempTemplatePath);
YamlHelper settingsFile = new YamlHelper(settingTempPath); YamlHelper settingsFile = new YamlHelper(settingTempPath);
migrateEnterpriseEditionToPremium(settingsFile, settingsTemplateFile);
boolean changesMade = boolean changesMade =
settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile); settingsTemplateFile.updateValuesFromYaml(settingsFile, settingsTemplateFile);
if (changesMade) { if (changesMade) {
@ -79,46 +76,4 @@ public class ConfigInitializer {
log.info("Created custom_settings file: {}", customSettingsPath.toString()); 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.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -22,14 +22,10 @@ public class EndpointConfiguration {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>(); private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
private final boolean runningProOrHigher;
@Autowired @Autowired
public EndpointConfiguration( public EndpointConfiguration(ApplicationProperties applicationProperties) {
ApplicationProperties applicationProperties,
@Qualifier("runningProOrHigher") boolean runningProOrHigher) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.runningProOrHigher = runningProOrHigher;
init(); init();
processEnvironmentConfigs(); processEnvironmentConfigs();
} }
@ -45,10 +41,6 @@ public class EndpointConfiguration {
} }
} }
public Map<String, Boolean> getEndpointStatuses() {
return endpointStatuses;
}
public boolean isEndpointEnabled(String endpoint) { public boolean isEndpointEnabled(String endpoint) {
if (endpoint.startsWith("/")) { if (endpoint.startsWith("/")) {
endpoint = endpoint.substring(1); endpoint = endpoint.substring(1);
@ -56,22 +48,6 @@ public class EndpointConfiguration {
return endpointStatuses.getOrDefault(endpoint, true); 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) { public void addEndpointToGroup(String group, String endpoint) {
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
} }
@ -101,7 +77,7 @@ public class EndpointConfiguration {
// is false) // is false)
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.sorted() .sorted()
.toList(); .collect(Collectors.toList());
if (!disabledList.isEmpty()) { if (!disabledList.isEmpty()) {
log.info( log.info(
@ -188,8 +164,14 @@ public class EndpointConfiguration {
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf"); addEndpointToGroup("CLI", "url-to-pdf");
addEndpointToGroup("CLI", "book-to-pdf");
addEndpointToGroup("CLI", "pdf-to-book");
addEndpointToGroup("CLI", "pdf-to-rtf"); addEndpointToGroup("CLI", "pdf-to-rtf");
// Calibre
addEndpointToGroup("Calibre", "book-to-pdf");
addEndpointToGroup("Calibre", "pdf-to-book");
// python // python
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
@ -200,17 +182,21 @@ public class EndpointConfiguration {
addEndpointToGroup("OpenCV", "extract-image-scans"); addEndpointToGroup("OpenCV", "extract-image-scans");
// LibreOffice // LibreOffice
addEndpointToGroup("qpdf", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); addEndpointToGroup("LibreOffice", "file-to-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("LibreOffice", "pdf-to-rtf"); addEndpointToGroup("LibreOffice", "pdf-to-rtf");
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("LibreOffice", "pdf-to-xml");
addEndpointToGroup("LibreOffice", "pdf-to-pdfa");
// Unoconvert // Unoconvert
addEndpointToGroup("Unoconvert", "file-to-pdf"); addEndpointToGroup("Unoconvert", "file-to-pdf");
// qpdf
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("tesseract", "ocr-pdf"); addEndpointToGroup("tesseract", "ocr-pdf");
// Java // Java
@ -260,6 +246,8 @@ public class EndpointConfiguration {
addEndpointToGroup("Javascript", "adjust-contrast"); addEndpointToGroup("Javascript", "adjust-contrast");
// qpdf dependent endpoints // qpdf dependent endpoints
addEndpointToGroup("qpdf", "compress-pdf");
addEndpointToGroup("qpdf", "pdf-to-pdfa");
addEndpointToGroup("qpdf", "repair"); addEndpointToGroup("qpdf", "repair");
// Weasyprint dependent endpoints // 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) { 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.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
@Component @Component
@Slf4j
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
private final EndpointConfiguration endpointConfiguration; private final EndpointConfiguration endpointConfiguration;
@ -23,29 +20,7 @@ public class EndpointInterceptor implements HandlerInterceptor {
HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
boolean isEnabled; if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
// 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) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false; 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) { private List<String> getAffectedFeatures(String group) {
return endpointConfiguration.getEndpointsForGroup(group).stream() return endpointConfiguration.getEndpointsForGroup(group).stream()
.map(endpoint -> formatEndpointAsFeature(endpoint)) .map(endpoint -> formatEndpointAsFeature(endpoint))
.toList(); .collect(Collectors.toList());
} }
private String formatEndpointAsFeature(String endpoint) { private String formatEndpointAsFeature(String endpoint) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,9 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDDocumentCatalog; import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
@ -32,7 +34,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; 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.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -42,10 +44,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MergeController { public class MergeController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public MergeController(CustomPDFDocumentFactory pdfDocumentFactory) { public MergeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -99,8 +101,8 @@ public class MergeController {
}; };
case "byPDFTitle": case "byPDFTitle":
return (file1, file2) -> { return (file1, file2) -> {
try (PDDocument doc1 = pdfDocumentFactory.load(file1); try (PDDocument doc1 = Loader.loadPDF(file1.getBytes());
PDDocument doc2 = pdfDocumentFactory.load(file2)) { PDDocument doc2 = Loader.loadPDF(file2.getBytes())) {
String title1 = doc1.getDocumentInformation().getTitle(); String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2); return title1.compareTo(title2);
@ -118,13 +120,12 @@ public class MergeController {
@Operation( @Operation(
summary = "Merge multiple PDF files into one", summary = "Merge multiple PDF files into one",
description = description =
"This endpoint merges multiple PDF files into a single PDF file. The merged" "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")
+ " 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) public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException { throws IOException {
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete 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; PDDocument mergedDocument = null;
boolean removeCertSign = form.isRemoveCertSign(); boolean removeCertSign = form.isRemoveCertSign();
@ -137,24 +138,21 @@ public class MergeController {
form.getSortType())); // Sort files based on the given sort type form.getSortType())); // Sort files based on the given sort type
PDFMergerUtility mergerUtility = new PDFMergerUtility(); PDFMergerUtility mergerUtility = new PDFMergerUtility();
long totalSize = 0;
for (MultipartFile multipartFile : files) { for (MultipartFile multipartFile : files) {
totalSize += multipartFile.getSize();
File tempFile = File tempFile =
GeneralUtils.convertMultipartFileToFile( GeneralUtils.convertMultipartFileToFile(
multipartFile); // Convert MultipartFile to File multipartFile); // Convert MultipartFile to File
filesToDelete.add(tempFile); // Add temp file to the list for later deletion filesToDelete.add(tempFile); // Add temp file to the list for later deletion
mergerUtility.addSource(tempFile); // Add source file to the merger utility 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(); byte[] mergedPdfBytes = docOutputstream.toByteArray(); // Get merged document bytes
mergerUtility.setDestinationFileName(mergedTempFile.getAbsolutePath());
mergerUtility.mergeDocuments(
pdfDocumentFactory.getStreamCacheFunction(totalSize)); // Merge the documents
// Load the merged PDF document // Load the merged PDF document
mergedDocument = pdfDocumentFactory.load(mergedTempFile); mergedDocument = Loader.loadPDF(mergedPdfBytes);
// Remove signatures if removeCertSign is true // Remove signatures if removeCertSign is true
if (removeCertSign) { if (removeCertSign) {
@ -164,7 +162,7 @@ public class MergeController {
List<PDField> fieldsToRemove = List<PDField> fieldsToRemove =
acroForm.getFields().stream() acroForm.getFields().stream()
.filter(field -> field instanceof PDSignatureField) .filter(field -> field instanceof PDSignatureField)
.toList(); .collect(Collectors.toList());
if (!fieldsToRemove.isEmpty()) { if (!fieldsToRemove.isEmpty()) {
acroForm.flatten( acroForm.flatten(
@ -181,23 +179,21 @@ public class MergeController {
String mergedFileName = String mergedFileName =
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
+ "_merged_unsigned.pdf"; + "_merged_unsigned.pdf";
return WebResponseUtils.boasToWebResponse( return WebResponseUtils.bytesToWebResponse(
baos, mergedFileName); // Return the modified PDF baos.toByteArray(), mergedFileName); // Return the modified PDF
} catch (Exception ex) { } catch (Exception ex) {
log.error("Error in merge pdf process", ex); log.error("Error in merge pdf process", ex);
throw ex; throw ex;
} finally { } finally {
if (mergedDocument != null) {
mergedDocument.close(); // Close the merged document
}
for (File file : filesToDelete) { for (File file : filesToDelete) {
if (file != null) { if (file != null) {
Files.deleteIfExists(file.toPath()); // Delete temporary files Files.deleteIfExists(file.toPath()); // Delete temporary files
} }
} }
if (mergedTempFile != null) { docOutputstream.close();
Files.deleteIfExists(mergedTempFile.toPath()); 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.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -32,10 +33,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class MultiPageLayoutController { public class MultiPageLayoutController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public MultiPageLayoutController(CustomPDFDocumentFactory pdfDocumentFactory) { public MultiPageLayoutController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -43,8 +44,7 @@ public class MultiPageLayoutController {
@Operation( @Operation(
summary = "Merge multiple pages of a PDF document into a single page", summary = "Merge multiple pages of a PDF document into a single page",
description = description =
"This operation takes an input PDF file and the number of pages to merge into a" "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")
+ " single sheet in the output PDF file. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> mergeMultiplePagesIntoOne( public ResponseEntity<byte[]> mergeMultiplePagesIntoOne(
@ModelAttribute MergeMultiplePagesRequest request) throws IOException { @ModelAttribute MergeMultiplePagesRequest request) throws IOException {
@ -64,7 +64,7 @@ public class MultiPageLayoutController {
: (int) Math.sqrt(pagesPerSheet); : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (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 = PDDocument newDocument =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
PDPage newPage = new PDPage(PDRectangle.A4); 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; 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.service.PdfImageRemovalService;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -31,7 +31,7 @@ public class PdfImageRemovalController {
// Service for removing images from PDFs // Service for removing images from PDFs
private final PdfImageRemovalService pdfImageRemovalService; private final PdfImageRemovalService pdfImageRemovalService;
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
/** /**
* Constructor for dependency injection of PdfImageRemovalService. * Constructor for dependency injection of PdfImageRemovalService.
@ -41,7 +41,7 @@ public class PdfImageRemovalController {
@Autowired @Autowired
public PdfImageRemovalController( public PdfImageRemovalController(
PdfImageRemovalService pdfImageRemovalService, PdfImageRemovalService pdfImageRemovalService,
CustomPDFDocumentFactory pdfDocumentFactory) { CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfImageRemovalService = pdfImageRemovalService; this.pdfImageRemovalService = pdfImageRemovalService;
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -61,8 +61,7 @@ public class PdfImageRemovalController {
@Operation( @Operation(
summary = "Remove images from file to reduce the file size.", summary = "Remove images from file to reduce the file size.",
description = description =
"This endpoint remove images from file to reduce the file size.Input:PDF" "This endpoint remove images from file to reduce the file size.Input:PDF Output:PDF Type:MISO")
+ " Output:PDF Type:MISO")
public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException { public ResponseEntity<byte[]> removeImages(@ModelAttribute PDFFile file) throws IOException {
// Load the PDF document // Load the PDF document
PDDocument document = pdfDocumentFactory.load(file); 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest; 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.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -35,10 +35,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class PdfOverlayController { public class PdfOverlayController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public PdfOverlayController(CustomPDFDocumentFactory pdfDocumentFactory) { public PdfOverlayController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -46,8 +46,7 @@ public class PdfOverlayController {
@Operation( @Operation(
summary = "Overlay PDF files in various modes", summary = "Overlay PDF files in various modes",
description = description =
"Overlay PDF files onto a base PDF with different modes: Sequential," "Overlay PDF files onto a base PDF with different modes: Sequential, Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
+ " Interleaved, or Fixed Repeat. Input:PDF Output:PDF Type:MIMO")
public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request) public ResponseEntity<byte[]> overlayPdfs(@ModelAttribute OverlayPdfsRequest request)
throws IOException { throws IOException {
MultipartFile baseFile = request.getFileInput(); MultipartFile baseFile = request.getFileInput();

View File

@ -5,6 +5,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired; 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.SortTypes;
import stirling.software.SPDF.model.api.PDFWithPageNums; import stirling.software.SPDF.model.api.PDFWithPageNums;
import stirling.software.SPDF.model.api.general.RearrangePagesRequest; 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.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,10 +35,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RearrangePagesPDFController { public class RearrangePagesPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public RearrangePagesPDFController(CustomPDFDocumentFactory pdfDocumentFactory) { public RearrangePagesPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -45,9 +46,7 @@ public class RearrangePagesPDFController {
@Operation( @Operation(
summary = "Remove pages from a PDF file", summary = "Remove pages from a PDF file",
description = description =
"This endpoint removes specified pages from a given PDF file. Users can provide" "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")
+ " a comma-separated list of page numbers or ranges to delete. Input:PDF"
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request) public ResponseEntity<byte[]> deletePages(@ModelAttribute PDFWithPageNums request)
throws IOException { throws IOException {
@ -244,10 +243,7 @@ public class RearrangePagesPDFController {
@Operation( @Operation(
summary = "Rearrange pages in a PDF file", summary = "Rearrange pages in a PDF file",
description = description =
"This endpoint rearranges pages in a given PDF file based on the specified page" "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")
+ " 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) public ResponseEntity<byte[]> rearrangePages(@ModelAttribute RearrangePagesRequest request)
throws IOException { throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
@ -255,7 +251,7 @@ public class RearrangePagesPDFController {
String sortType = request.getCustomMode(); String sortType = request.getCustomMode();
try { try {
// Load the input PDF // 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 // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.RotatePDFRequest; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -26,10 +26,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class RotationController { public class RotationController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public RotationController(CustomPDFDocumentFactory pdfDocumentFactory) { public RotationController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -37,18 +37,11 @@ public class RotationController {
@Operation( @Operation(
summary = "Rotate a PDF file", summary = "Rotate a PDF file",
description = description =
"This endpoint rotates a given PDF file by a specified angle. The angle must be" "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")
+ " a multiple of 90. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request) public ResponseEntity<byte[]> rotatePDF(@ModelAttribute RotatePDFRequest request)
throws IOException { throws IOException {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
Integer angle = request.getAngle(); 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 // Load the PDF document
PDDocument document = pdfDocumentFactory.load(request); PDDocument document = pdfDocumentFactory.load(request);

View File

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

View File

@ -1,12 +1,10 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.util.Map;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.Hidden;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.config.InstallationPathConfig; import stirling.software.SPDF.config.InstallationPathConfig;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.utils.GeneralUtils; import stirling.software.SPDF.utils.GeneralUtils;
@ -26,13 +23,9 @@ import stirling.software.SPDF.utils.GeneralUtils;
public class SettingsController { public class SettingsController {
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final EndpointConfiguration endpointConfiguration;
public SettingsController( public SettingsController(ApplicationProperties applicationProperties) {
ApplicationProperties applicationProperties,
EndpointConfiguration endpointConfiguration) {
this.applicationProperties = applicationProperties; this.applicationProperties = applicationProperties;
this.endpointConfiguration = endpointConfiguration;
} }
@PostMapping("/update-enable-analytics") @PostMapping("/update-enable-analytics")
@ -48,10 +41,4 @@ public class SettingsController {
applicationProperties.getSystem().setEnableAnalytics(enabled); applicationProperties.getSystem().setEnableAnalytics(enabled);
return ResponseEntity.ok("Updated"); 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.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFWithPageNums; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -37,10 +38,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPDFController { public class SplitPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public SplitPDFController(CustomPDFDocumentFactory pdfDocumentFactory) { public SplitPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -48,10 +49,7 @@ public class SplitPDFController {
@Operation( @Operation(
summary = "Split a PDF file into separate documents", summary = "Split a PDF file into separate documents",
description = description =
"This endpoint splits a given PDF file into separate documents based on the" "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")
+ " 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) public ResponseEntity<byte[]> splitPdf(@ModelAttribute PDFWithPageNums request)
throws IOException { throws IOException {
@ -65,7 +63,7 @@ public class SplitPDFController {
String pages = request.getPageNumbers(); String pages = request.getPageNumbers();
// open the pdf document // open the pdf document
document = pdfDocumentFactory.load(file); document = Loader.loadPDF(file.getBytes());
// PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document); // PdfMetadata metadata = PdfMetadataService.extractMetadataFromPdf(document);
int totalPages = document.getNumberOfPages(); int totalPages = document.getNumberOfPages();
List<Integer> pageNumbers = request.getPageNumbersList(document, false); 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.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline; 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.PdfMetadata;
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest; import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.service.PdfMetadataService; import stirling.software.SPDF.service.PdfMetadataService;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -45,13 +45,9 @@ public class SplitPdfByChaptersController {
private final PdfMetadataService pdfMetadataService; private final PdfMetadataService pdfMetadataService;
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public SplitPdfByChaptersController( public SplitPdfByChaptersController(PdfMetadataService pdfMetadataService) {
PdfMetadataService pdfMetadataService, CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfMetadataService = pdfMetadataService; this.pdfMetadataService = pdfMetadataService;
this.pdfDocumentFactory = pdfDocumentFactory;
} }
private static List<Bookmark> extractOutlineItems( private static List<Bookmark> extractOutlineItems(
@ -139,7 +135,7 @@ public class SplitPdfByChaptersController {
if (bookmarkLevel < 0) { if (bookmarkLevel < 0) {
return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes()); return ResponseEntity.badRequest().body("Invalid bookmark level".getBytes());
} }
sourceDocument = pdfDocumentFactory.load(file); sourceDocument = Loader.loadPDF(file.getBytes());
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline(); PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();

View File

@ -9,6 +9,7 @@ import java.util.List;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -39,10 +40,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySectionsController { public class SplitPdfBySectionsController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public SplitPdfBySectionsController(CustomPDFDocumentFactory pdfDocumentFactory) { public SplitPdfBySectionsController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -50,15 +51,13 @@ public class SplitPdfBySectionsController {
@Operation( @Operation(
summary = "Split PDF pages into smaller sections", summary = "Split PDF pages into smaller sections",
description = description =
"Split each page of a PDF into smaller sections based on the user's choice" "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")
+ " (halves, thirds, quarters, etc.), both vertically and horizontally."
+ " Input:PDF Output:ZIP-PDF Type:SISO")
public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request) public ResponseEntity<byte[]> splitPdf(@ModelAttribute SplitPdfBySectionsRequest request)
throws Exception { throws Exception {
List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>(); List<ByteArrayOutputStream> splitDocumentsBoas = new ArrayList<>();
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument sourceDocument = pdfDocumentFactory.load(file); PDDocument sourceDocument = Loader.loadPDF(file.getBytes());
// Process the PDF based on split parameters // Process the PDF based on split parameters
int horiz = request.getHorizontalDivisions() + 1; 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.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.springframework.beans.factory.annotation.Autowired; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest; 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.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -35,10 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class SplitPdfBySizeController { public class SplitPdfBySizeController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public SplitPdfBySizeController(CustomPDFDocumentFactory pdfDocumentFactory) { public SplitPdfBySizeController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -46,96 +47,43 @@ public class SplitPdfBySizeController {
@Operation( @Operation(
summary = "Auto split PDF pages into separate documents based on size or count", summary = "Auto split PDF pages into separate documents based on size or count",
description = description =
"split PDF into multiple paged documents based on size/count, ie if 20 pages" "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"
+ " 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")
+ " 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) public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute SplitPdfBySizeOrCountRequest request)
throws Exception { throws Exception {
log.debug("Starting PDF split process with request: {}", request);
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Path zipFile = Files.createTempFile("split_documents", ".zip"); Path zipFile = Files.createTempFile("split_documents", ".zip");
log.debug("Created temporary zip file: {}", zipFile);
String filename = String filename =
Filenames.toSimpleFileName(file.getOriginalFilename()) Filenames.toSimpleFileName(file.getOriginalFilename())
.replaceFirst("[.][^.]+$", ""); .replaceFirst("[.][^.]+$", "");
log.debug("Base filename for output: {}", filename);
byte[] data = null; byte[] data = null;
try { try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile));
log.debug("Reading input file bytes"); PDDocument sourceDocument = Loader.loadPDF(file.getBytes())) {
byte[] pdfBytes = file.getBytes();
log.debug("Successfully read {} bytes from input file", pdfBytes.length);
log.debug("Creating ZIP output stream"); int type = request.getSplitType();
try (ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipFile))) { String value = request.getSplitValue();
log.debug("Loading PDF document");
try (PDDocument sourceDocument = pdfDocumentFactory.load(pdfBytes)) {
log.debug(
"Successfully loaded PDF with {} pages",
sourceDocument.getNumberOfPages());
int type = request.getSplitType(); if (type == 0) {
String value = request.getSplitValue(); long maxBytes = GeneralUtils.convertSizeToBytes(value);
log.debug("Split type: {}, Split value: {}", type, value); handleSplitBySize(sourceDocument, maxBytes, zipOut, filename);
} else if (type == 1) {
if (type == 0) { int pageCount = Integer.parseInt(value);
log.debug("Processing split by size"); handleSplitByPageCount(sourceDocument, pageCount, zipOut, filename);
long maxBytes = GeneralUtils.convertSizeToBytes(value); } else if (type == 2) {
log.debug("Max bytes per document: {}", maxBytes); int documentCount = Integer.parseInt(value);
handleSplitBySize(sourceDocument, maxBytes, zipOut, filename); handleSplitByDocCount(sourceDocument, documentCount, zipOut, filename);
} else if (type == 1) { } else {
log.debug("Processing split by page count"); throw new IllegalArgumentException("Invalid argument for split type");
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;
} }
} catch (Exception e) { } catch (Exception e) {
log.error("Exception during PDF splitting process", e); log.error("exception", e);
throw e; // Re-throw to ensure proper error response
} finally { } finally {
try { data = Files.readAllBytes(zipFile);
log.debug("Reading ZIP file data"); Files.deleteIfExists(zipFile);
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);
}
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( return WebResponseUtils.bytesToWebResponse(
data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM); data, filename + ".zip", MediaType.APPLICATION_OCTET_STREAM);
} }
@ -143,230 +91,63 @@ public class SplitPdfBySizeController {
private void handleSplitBySize( private void handleSplitBySize(
PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename) PDDocument sourceDocument, long maxBytes, ZipOutputStream zipOut, String baseFilename)
throws IOException { throws IOException {
log.debug("Starting handleSplitBySize with maxBytes={}", maxBytes); long currentSize = 0;
PDDocument currentDoc = PDDocument currentDoc =
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument); pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
int fileIndex = 1; int fileIndex = 1;
int totalPages = sourceDocument.getNumberOfPages();
int pageAdded = 0;
// Smart size check frequency - check more often with larger documents for (int pageIndex = 0; pageIndex < sourceDocument.getNumberOfPages(); pageIndex++) {
int baseCheckFrequency = 5;
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
PDPage page = sourceDocument.getPage(pageIndex); PDPage page = sourceDocument.getPage(pageIndex);
log.debug("Processing page {} of {}", pageIndex + 1, totalPages); ByteArrayOutputStream pageOutputStream = new ByteArrayOutputStream();
// Add the page to current document try (PDDocument tempDoc = new PDDocument()) {
PDPage newPage = new PDPage(page.getCOSObject()); PDPage importedPage = tempDoc.importPage(page); // This creates a new PDPage object
currentDoc.addPage(newPage); tempDoc.save(pageOutputStream);
pageAdded++; }
// Dynamic size checking based on document size and page count long pageSize = pageOutputStream.size();
boolean shouldCheckSize = if (currentSize + pageSize > maxBytes) {
(pageAdded % baseCheckFrequency == 0) if (currentDoc.getNumberOfPages() > 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");
}
log.debug(
"Saving document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc.close(); // Make sure to close the document
currentDoc = new PDDocument(); currentDoc = new PDDocument();
pageAdded = 0; currentSize = 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;
}
}
}
} }
} }
PDPage newPage = new PDPage(page.getCOSObject()); // Re-create the page
currentDoc.addPage(newPage);
currentSize += pageSize;
} }
// Save final document if it has any pages if (currentDoc.getNumberOfPages() != 0) {
if (currentDoc.getNumberOfPages() > 0) {
log.debug(
"Saving final document with {} pages as part {}",
currentDoc.getNumberOfPages(),
fileIndex);
saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++); saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
currentDoc.close();
} }
log.debug("Completed handleSplitBySize with {} document parts created", fileIndex - 1);
} }
private void handleSplitByPageCount( private void handleSplitByPageCount(
PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename) PDDocument sourceDocument, int pageCount, ZipOutputStream zipOut, String baseFilename)
throws IOException { throws IOException {
log.debug("Starting handleSplitByPageCount with pageCount={}", pageCount);
int currentPageCount = 0; int currentPageCount = 0;
log.debug("Creating initial output document"); PDDocument currentDoc =
PDDocument currentDoc = null; pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
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);
}
int fileIndex = 1; int fileIndex = 1;
int pageIndex = 0; for (PDPage page : sourceDocument.getPages()) {
int totalPages = sourceDocument.getNumberOfPages(); currentDoc.addPage(page);
log.debug("Processing {} pages", totalPages); currentPageCount++;
try { if (currentPageCount == pageCount) {
for (PDPage page : sourceDocument.getPages()) { // Save and reset current document
pageIndex++; saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
log.debug("Processing page {} of {}", pageIndex, totalPages); currentDoc = new PDDocument();
currentPageCount = 0;
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 {
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 // Add the last document if it contains any pages
try { if (currentDoc.getPages().getCount() != 0) {
if (currentDoc.getPages().getCount() != 0) { saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
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( private void handleSplitByDocCount(
@ -375,101 +156,35 @@ public class SplitPdfBySizeController {
ZipOutputStream zipOut, ZipOutputStream zipOut,
String baseFilename) String baseFilename)
throws IOException { throws IOException {
log.debug("Starting handleSplitByDocCount with documentCount={}", documentCount);
int totalPageCount = sourceDocument.getNumberOfPages(); int totalPageCount = sourceDocument.getNumberOfPages();
log.debug("Total pages in source document: {}", totalPageCount);
int pagesPerDocument = totalPageCount / documentCount; int pagesPerDocument = totalPageCount / documentCount;
int extraPages = totalPageCount % documentCount; int extraPages = totalPageCount % documentCount;
log.debug("Pages per document: {}, Extra pages: {}", pagesPerDocument, extraPages);
int currentPageIndex = 0; int currentPageIndex = 0;
int fileIndex = 1; int fileIndex = 1;
for (int i = 0; i < documentCount; i++) { for (int i = 0; i < documentCount; i++) {
log.debug("Creating document {} of {}", i + 1, documentCount); PDDocument currentDoc =
PDDocument currentDoc = null; pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument);
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);
}
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0); int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
log.debug("Adding {} pages to document {}", pagesToAdd, i + 1);
for (int j = 0; j < pagesToAdd; j++) { for (int j = 0; j < pagesToAdd; j++) {
try { currentDoc.addPage(sourceDocument.getPage(currentPageIndex++));
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);
}
} }
try { saveDocumentToZip(currentDoc, zipOut, baseFilename, fileIndex++);
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( private void saveDocumentToZip(
PDDocument document, ZipOutputStream zipOut, String baseFilename, int index) PDDocument document, ZipOutputStream zipOut, String baseFilename, int index)
throws IOException { throws IOException {
log.debug("Starting saveDocumentToZip for document part {}", index);
ByteArrayOutputStream outStream = new ByteArrayOutputStream(); ByteArrayOutputStream outStream = new ByteArrayOutputStream();
document.save(outStream);
document.close(); // Close the document to free resources
try { // Create a new zip entry
log.debug("Saving document part {} to byte array", index); ZipEntry zipEntry = new ZipEntry(baseFilename + "_" + index + ".pdf");
document.save(outStream); zipOut.putNextEntry(zipEntry);
log.debug("Successfully saved document part {} ({} bytes)", index, outStream.size()); zipOut.write(outStream.toByteArray());
} catch (Exception e) { zipOut.closeEntry();
log.error("Error saving document part {} to byte array", index, e);
throw new IOException("Failed to save document to byte array", e);
}
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);
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.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.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; 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 io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.PDFFile; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -29,10 +30,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "General", description = "General APIs") @Tag(name = "General", description = "General APIs")
public class ToSinglePageController { public class ToSinglePageController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public ToSinglePageController(CustomPDFDocumentFactory pdfDocumentFactory) { public ToSinglePageController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -40,15 +41,12 @@ public class ToSinglePageController {
@Operation( @Operation(
summary = "Convert a multi-page PDF into a single long page PDF", summary = "Convert a multi-page PDF into a single long page PDF",
description = description =
"This endpoint converts a multi-page PDF document into a single paged PDF" "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")
+ " 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) public ResponseEntity<byte[]> pdfToSinglePage(@ModelAttribute PDFFile request)
throws IOException { throws IOException {
// Load the source document // Load the source document
PDDocument sourceDocument = pdfDocumentFactory.load(request); PDDocument sourceDocument = Loader.loadPDF(request.getFileInput().getBytes());
// Calculate total height and max width // Calculate total height and max width
float totalHeight = 0; 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.UserService;
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; 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.AuthenticationType;
import stirling.software.SPDF.model.Role; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
@ -48,15 +47,10 @@ public class UserController {
private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated"; private static final String LOGIN_MESSAGETYPE_CREDSUPDATED = "/login?messageType=credsUpdated";
private final UserService userService; private final UserService userService;
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
public UserController( public UserController(UserService userService, SessionPersistentRegistry sessionRegistry) {
UserService userService,
SessionPersistentRegistry sessionRegistry,
ApplicationProperties applicationProperties) {
this.userService = userService; this.userService = userService;
this.sessionRegistry = sessionRegistry; this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties;
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@ -200,44 +194,39 @@ public class UserController {
boolean forceChange) boolean forceChange)
throws IllegalArgumentException, SQLException, UnsupportedProviderException { throws IllegalArgumentException, SQLException, UnsupportedProviderException {
if (!userService.isUsernameValid(username)) { if (!userService.isUsernameValid(username)) {
return new RedirectView("/adminSettings?messageType=invalidUsername", true); return new RedirectView("/addUsers?messageType=invalidUsername", true);
}
if (applicationProperties.getPremium().isEnabled()
&& applicationProperties.getPremium().getMaxUsers()
<= userService.getTotalUsersCount()) {
return new RedirectView("/adminSettings?messageType=maxUsersReached", true);
} }
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) { if (userOpt.isPresent()) {
User user = userOpt.get(); User user = userOpt.get();
if (user.getUsername().equalsIgnoreCase(username)) { if (user.getUsername().equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=usernameExists", true); return new RedirectView("/addUsers?messageType=usernameExists", true);
} }
} }
if (userService.usernameExistsIgnoreCase(username)) { if (userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=usernameExists", true); return new RedirectView("/addUsers?messageType=usernameExists", true);
} }
try { try {
// Validate the role // Validate the role
Role roleEnum = Role.fromString(role); Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) { if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request // 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) { } catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message // 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())) { if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, role); userService.saveUser(username, AuthenticationType.SSO, role);
} else { } else {
if (password.isBlank()) { if (password.isBlank()) {
return new RedirectView("/adminSettings?messageType=invalidPassword", true); return new RedirectView("/addUsers?messageType=invalidPassword", true);
} }
userService.saveUser(username, password, role, forceChange); userService.saveUser(username, password, role, forceChange);
} }
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true); true);
} }
@ -250,32 +239,32 @@ public class UserController {
throws SQLException, UnsupportedProviderException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (!userOpt.isPresent()) { if (!userOpt.isPresent()) {
return new RedirectView("/adminSettings?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=downgradeCurrentUser", true); return new RedirectView("/addUsers?messageType=downgradeCurrentUser", true);
} }
try { try {
// Validate the role // Validate the role
Role roleEnum = Role.fromString(role); Role roleEnum = Role.fromString(role);
if (roleEnum == Role.INTERNAL_API_USER) { if (roleEnum == Role.INTERNAL_API_USER) {
// If the role is INTERNAL_API_USER, reject the request // 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) { } catch (IllegalArgumentException e) {
// If the role ID is not valid, redirect with an error message // 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(); User user = userOpt.get();
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true); true);
} }
@ -288,16 +277,16 @@ public class UserController {
throws SQLException, UnsupportedProviderException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return new RedirectView("/adminSettings?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=userNotFound", true); return new RedirectView("/addUsers?messageType=userNotFound", true);
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(username)) { if (currentUsername.equalsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=disabledCurrentUser", true); return new RedirectView("/addUsers?messageType=disabledCurrentUser", true);
} }
User user = userOpt.get(); User user = userOpt.get();
userService.changeUserEnabled(user, enabled); userService.changeUserEnabled(user, enabled);
@ -325,7 +314,7 @@ public class UserController {
} }
} }
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/addUsers", // Redirect to account page after adding the user
true); true);
} }
@ -334,23 +323,23 @@ public class UserController {
public RedirectView deleteUser( public RedirectView deleteUser(
@PathVariable("username") String username, Authentication authentication) { @PathVariable("username") String username, Authentication authentication) {
if (!userService.usernameExistsIgnoreCase(username)) { if (!userService.usernameExistsIgnoreCase(username)) {
return new RedirectView("/adminSettings?messageType=deleteUsernameExists", true); return new RedirectView("/addUsers?messageType=deleteUsernameExists", true);
} }
// Get the currently authenticated username // Get the currently authenticated username
String currentUsername = authentication.getName(); String currentUsername = authentication.getName();
// Check if the provided username matches the current session's username // Check if the provided username matches the current session's username
if (currentUsername.equalsIgnoreCase(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 // Invalidate all sessions before deleting the user
List<SessionInformation> sessionsInformations = List<SessionInformation> sessionsInformations =
sessionRegistry.getAllSessions(username, false); sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
for (SessionInformation sessionsInformation : sessionsInformations) { for (SessionInformation sessionsInformation : sessionsInformations) {
sessionRegistry.expireSession(sessionsInformation.getSessionId()); sessionRegistry.expireSession(sessionsInformation.getSessionId());
sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId()); sessionRegistry.removeSessionInformation(sessionsInformation.getSessionId());
} }
userService.deleteUser(username); userService.deleteUser(username);
return new RedirectView("/adminSettings", true); return new RedirectView("/addUsers", true);
} }
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @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.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.converters.HTMLToPdfRequest; 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.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -24,7 +24,7 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertHtmlToPDF { public class ConvertHtmlToPDF {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
@ -32,7 +32,7 @@ public class ConvertHtmlToPDF {
@Autowired @Autowired
public ConvertHtmlToPDF( public ConvertHtmlToPDF(
CustomPDFDocumentFactory pdfDocumentFactory, CustomPDDocumentFactory pdfDocumentFactory,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
RuntimePathConfig runtimePathConfig) { RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
@ -45,8 +45,7 @@ public class ConvertHtmlToPDF {
@Operation( @Operation(
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF", summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
description = description =
"This endpoint takes an HTML or ZIP file input and converts it to a PDF format." "This endpoint takes an HTML or ZIP file input and converts it to a PDF format. Input:HTML Output:PDF Type:SISO")
+ " Input:HTML Output:PDF Type:SISO")
public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request) public ResponseEntity<byte[]> HtmlToPdf(@ModelAttribute HTMLToPdfRequest request)
throws Exception { throws Exception {
MultipartFile fileInput = request.getFileInput(); MultipartFile fileInput = request.getFileInput();

View File

@ -8,10 +8,12 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.rendering.ImageType; 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.ConvertToImageRequest;
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest; 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.*;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -42,10 +44,10 @@ import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@Tag(name = "Convert", description = "Convert APIs") @Tag(name = "Convert", description = "Convert APIs")
public class ConvertImgPDFController { public class ConvertImgPDFController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public ConvertImgPDFController(CustomPDFDocumentFactory pdfDocumentFactory) { public ConvertImgPDFController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -53,9 +55,7 @@ public class ConvertImgPDFController {
@Operation( @Operation(
summary = "Convert PDF to image(s)", summary = "Convert PDF to image(s)",
description = description =
"This endpoint converts a PDF file to image(s) with the specified image format," "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")
+ " 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) public ResponseEntity<byte[]> convertToImage(@ModelAttribute ConvertToImageRequest request)
throws NumberFormatException, Exception { throws NumberFormatException, Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
@ -75,7 +75,7 @@ public class ConvertImgPDFController {
; ;
try { try {
// Load the input PDF // Load the input PDF
byte[] newPdfBytes = rearrangePdfPages(file, pageOrderArr); byte[] newPdfBytes = rearrangePdfPages(file.getBytes(), pageOrderArr);
ImageType colorTypeResult = ImageType.RGB; ImageType colorTypeResult = ImageType.RGB;
if ("greyscale".equals(colorType)) { if ("greyscale".equals(colorType)) {
@ -91,7 +91,6 @@ public class ConvertImgPDFController {
result = result =
PdfUtils.convertFromPdf( PdfUtils.convertFromPdf(
pdfDocumentFactory,
newPdfBytes, newPdfBytes,
"webp".equalsIgnoreCase(imageFormat) "webp".equalsIgnoreCase(imageFormat)
? "png" ? "png"
@ -145,7 +144,7 @@ public class ConvertImgPDFController {
List<Path> webpFiles = List<Path> webpFiles =
Files.walk(tempOutputDir) Files.walk(tempOutputDir)
.filter(path -> path.toString().endsWith(".webp")) .filter(path -> path.toString().endsWith(".webp"))
.toList(); .collect(Collectors.toList());
if (webpFiles.isEmpty()) { if (webpFiles.isEmpty()) {
log.error("No WebP files were created in: {}", tempOutputDir.toString()); log.error("No WebP files were created in: {}", tempOutputDir.toString());
@ -209,9 +208,7 @@ public class ConvertImgPDFController {
@Operation( @Operation(
summary = "Convert images to a PDF file", summary = "Convert images to a PDF file",
description = description =
"This endpoint converts one or more images to a PDF file. Users can specify" "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")
+ " 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) public ResponseEntity<byte[]> convertToPdf(@ModelAttribute ConvertToPdfRequest request)
throws IOException { throws IOException {
MultipartFile[] file = request.getFileInput(); MultipartFile[] file = request.getFileInput();
@ -246,10 +243,9 @@ public class ConvertImgPDFController {
* @return A byte array of the rearranged PDF. * @return A byte array of the rearranged PDF.
* @throws IOException If an error occurs while processing the PDF. * @throws IOException If an error occurs while processing the PDF.
*/ */
private byte[] rearrangePdfPages(MultipartFile pdfFile, String[] pageOrderArr) private byte[] rearrangePdfPages(byte[] pdfBytes, String[] pageOrderArr) throws IOException {
throws IOException {
// Load the input PDF // Load the input PDF
PDDocument document = pdfDocumentFactory.load(pdfFile); PDDocument document = Loader.loadPDF(pdfBytes);
int totalPages = document.getNumberOfPages(); int totalPages = document.getNumberOfPages();
List<Integer> newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false); 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.config.RuntimePathConfig;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.api.GeneralFile; 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.FileToPdf;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,14 +34,14 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertMarkdownToPdf { public class ConvertMarkdownToPdf {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
@Autowired @Autowired
public ConvertMarkdownToPdf( public ConvertMarkdownToPdf(
CustomPDFDocumentFactory pdfDocumentFactory, CustomPDDocumentFactory pdfDocumentFactory,
ApplicationProperties applicationProperties, ApplicationProperties applicationProperties,
RuntimePathConfig runtimePathConfig) { RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
@ -54,8 +54,7 @@ public class ConvertMarkdownToPdf {
@Operation( @Operation(
summary = "Convert a Markdown file to PDF", summary = "Convert a Markdown file to PDF",
description = description =
"This endpoint takes a Markdown file input, converts it to HTML, and then to" "This endpoint takes a Markdown file input, converts it to HTML, and then to PDF format. Input:MARKDOWN Output:PDF Type:SISO")
+ " PDF format. Input:MARKDOWN Output:PDF Type:SISO")
public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request) public ResponseEntity<byte[]> markdownToPdf(@ModelAttribute GeneralFile request)
throws Exception { throws Exception {
MultipartFile fileInput = request.getFileInput(); 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.config.RuntimePathConfig;
import stirling.software.SPDF.model.api.GeneralFile; 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;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -34,12 +34,12 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@RequestMapping("/api/v1/convert") @RequestMapping("/api/v1/convert")
public class ConvertOfficeController { public class ConvertOfficeController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final RuntimePathConfig runtimePathConfig; private final RuntimePathConfig runtimePathConfig;
@Autowired @Autowired
public ConvertOfficeController( public ConvertOfficeController(
CustomPDFDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) { CustomPDDocumentFactory pdfDocumentFactory, RuntimePathConfig runtimePathConfig) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
this.runtimePathConfig = runtimePathConfig; this.runtimePathConfig = runtimePathConfig;
} }
@ -93,8 +93,7 @@ public class ConvertOfficeController {
@Operation( @Operation(
summary = "Convert a file to a PDF using LibreOffice", summary = "Convert a file to a PDF using LibreOffice",
description = description =
"This endpoint converts a given file to a PDF using LibreOffice API Input:ANY" "This endpoint converts a given file to a PDF using LibreOffice API Input:ANY Output:PDF Type:SISO")
+ " Output:PDF Type:SISO")
public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request) public ResponseEntity<byte[]> processFileToPDF(@ModelAttribute GeneralFile request)
throws Exception { throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();

View File

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

View File

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

View File

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

View File

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

View File

@ -5,10 +5,10 @@ import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper; import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition; import org.apache.pdfbox.text.TextPosition;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest; import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -35,25 +34,17 @@ public class AutoRenameController {
private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f; private static final float TITLE_FONT_SIZE_THRESHOLD = 20.0f;
private static final int LINE_LIMIT = 200; 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") @PostMapping(consumes = "multipart/form-data", value = "/auto-rename")
@Operation( @Operation(
summary = "Extract header from PDF file", summary = "Extract header from PDF file",
description = description =
"This endpoint accepts a PDF file and attempts to extract its title or header" "This endpoint accepts a PDF file and attempts to extract its title or header based on heuristics. Input:PDF Output:PDF Type:SISO")
+ " based on heuristics. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request) public ResponseEntity<byte[]> extractHeader(@ModelAttribute ExtractHeaderRequest request)
throws Exception { throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback(); Boolean useFirstTextAsFallback = request.isUseFirstTextAsFallback();
PDDocument document = pdfDocumentFactory.load(file); PDDocument document = Loader.loadPDF(file.getBytes());
PDFTextStripper reader = PDFTextStripper reader =
new PDFTextStripper() { new PDFTextStripper() {
List<LineInfo> lineInfos = new ArrayList<>(); 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -51,10 +51,10 @@ public class AutoSplitPdfController {
"https://github.com/Frooodle/Stirling-PDF", "https://github.com/Frooodle/Stirling-PDF",
"https://stirlingpdf.com")); "https://stirlingpdf.com"));
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public AutoSplitPdfController(CustomPDFDocumentFactory pdfDocumentFactory) { public AutoSplitPdfController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -111,9 +111,9 @@ public class AutoSplitPdfController {
summary = "Auto split PDF pages into separate documents", summary = "Auto split PDF pages into separate documents",
description = description =
"This endpoint accepts a PDF file, scans each page for a specific QR code, and" "This endpoint accepts a PDF file, scans each page for a specific QR code, and"
+ " splits the document at the QR code boundaries. The output is a zip file" + " splits the document at the QR code boundaries. The output is a zip file"
+ " containing each separate PDF document. Input:PDF Output:ZIP-PDF" + " containing each separate PDF document. Input:PDF Output:ZIP-PDF"
+ " Type:SISO") + " Type:SISO")
public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request) public ResponseEntity<byte[]> autoSplitPdf(@ModelAttribute AutoSplitPdfRequest request)
throws IOException { throws IOException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();

View File

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

View File

@ -3,35 +3,22 @@ package stirling.software.SPDF.controller.api.misc;
import java.awt.*; import java.awt.*;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.imageio.IIOImage;
import javax.imageio.ImageIO; 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.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.graphics.PDXObject; 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.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping; 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.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.EndpointConfiguration;
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest; 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.GeneralUtils;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -63,588 +45,73 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class CompressController { public class CompressController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
private final boolean qpdfEnabled;
public CompressController( @Autowired
CustomPDFDocumentFactory pdfDocumentFactory, public CompressController(CustomPDDocumentFactory pdfDocumentFactory) {
EndpointConfiguration endpointConfiguration) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
this.qpdfEnabled = endpointConfiguration.isGroupEnabled("qpdf");
} }
@Data private void compressImagesInPDF(Path pdfFile, double initialScaleFactor, boolean grayScale)
@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)
throws Exception { throws Exception {
Path newCompressedPDF = Files.createTempFile("compressedPDF", ".pdf"); byte[] fileBytes = Files.readAllBytes(pdfFile);
long originalFileSize = Files.size(pdfFile); try (PDDocument doc = Loader.loadPDF(fileBytes)) {
log.info( double scaleFactor = initialScaleFactor;
"Starting image compression with scale factor: {}, JPEG quality: {}, grayscale: {} on file size: {}",
scaleFactor,
jpegQuality,
convertToGrayscale,
GeneralUtils.formatBytes(originalFileSize));
try (PDDocument doc = pdfDocumentFactory.load(pdfFile)) { for (PDPage page : doc.getPages()) {
// Find all unique images in the document PDResources res = page.getResources();
Map<String, List<ImageReference>> uniqueImages = findImages(doc); if (res != null && res.getXObjectNames() != null) {
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
if (xobj instanceof PDImageXObject image) {
BufferedImage bufferedImage = image.getImage();
// Get statistics int newWidth = (int) (bufferedImage.getWidth() * scaleFactor);
CompressionStats stats = new CompressionStats(); int newHeight = (int) (bufferedImage.getHeight() * scaleFactor);
stats.uniqueImagesCount = uniqueImages.size();
calculateImageStats(uniqueImages, stats);
// Create compressed versions of unique images if (newWidth == 0 || newHeight == 0) {
Map<String, PDImageXObject> compressedVersions = continue;
createCompressedImages( }
doc, uniqueImages, scaleFactor, jpegQuality, convertToGrayscale, stats);
// Replace all instances with compressed versions Image scaledImage =
replaceImages(doc, uniqueImages, compressedVersions, stats); bufferedImage.getScaledInstance(
newWidth, newHeight, Image.SCALE_SMOOTH);
// Log compression statistics BufferedImage scaledBufferedImage;
logCompressionStats(stats, originalFileSize); if (grayScale
|| bufferedImage.getType() == BufferedImage.TYPE_BYTE_GRAY) {
scaledBufferedImage =
new BufferedImage(
newWidth, newHeight, BufferedImage.TYPE_BYTE_GRAY);
scaledBufferedImage
.getGraphics()
.drawImage(scaledImage, 0, 0, null);
} else {
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();
// Free memory before saving PDImageXObject compressedImage =
compressedVersions.clear(); PDImageXObject.createFromByteArray(
uniqueImages.clear(); doc, imageBytes, image.getCOSObject().toString());
res.put(name, compressedImage);
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);
PDResources res = page.getResources();
if (res == null || res.getXObjectNames() == null) continue;
// Process all XObjects on the page
for (COSName name : res.getXObjectNames()) {
PDXObject xobj = res.getXObject(name);
// Direct image
if (isImage(xobj)) {
addDirectImage(pageNum, name, (PDImageXObject) xobj, uniqueImages);
log.info(
"Found direct image '{}' on page {} - {}x{}",
name.getName(),
pageNum + 1,
((PDImageXObject) xobj).getWidth(),
((PDImageXObject) xobj).getHeight());
}
// Form XObject that may contain nested images
else if (isForm(xobj)) {
checkFormForImages(pageNum, name, (PDFormXObject) xobj, uniqueImages);
} }
} }
Path tempOutput = Files.createTempFile("output_", ".pdf");
doc.save(tempOutput.toString());
Files.move(tempOutput, pdfFile, StandardCopyOption.REPLACE_EXISTING);
} }
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 {
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;
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;
}
// Convert to grayscale first if requested (before resizing for better quality)
if (convertToGrayscale) {
bufferedImage = convertToGrayscale(bufferedImage);
log.info("Converted image to grayscale");
}
// 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 =
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();
} else {
ImageIO.write(scaledImage, format, outputStream);
}
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);
}
}
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
};
}
// 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") @PostMapping(consumes = "multipart/form-data", value = "/compress-pdf")
@ -652,13 +119,13 @@ public class CompressController {
summary = "Optimize PDF file", summary = "Optimize PDF file",
description = description =
"This endpoint accepts a PDF file and optimizes it based on the provided" "This endpoint accepts a PDF file and optimizes it based on the provided"
+ " parameters. Input:PDF Output:PDF Type:SISO") + " parameters. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request) public ResponseEntity<byte[]> optimizePdf(@ModelAttribute OptimizePdfRequest request)
throws Exception { throws Exception {
MultipartFile inputFile = request.getFileInput(); MultipartFile inputFile = request.getFileInput();
Integer optimizeLevel = request.getOptimizeLevel(); Integer optimizeLevel = request.getOptimizeLevel();
String expectedOutputSizeString = request.getExpectedOutputSize(); String expectedOutputSizeString = request.getExpectedOutputSize();
Boolean convertToGrayscale = request.getGrayscale();
if (expectedOutputSizeString == null && optimizeLevel == null) { if (expectedOutputSizeString == null && optimizeLevel == null) {
throw new Exception("Both expected output size and optimize level are not specified"); throw new Exception("Both expected output size and optimize level are not specified");
} }
@ -670,189 +137,119 @@ public class CompressController {
autoMode = true; autoMode = true;
} }
// Create initial input file Path tempInputFile = Files.createTempFile("input_", ".pdf");
Path originalFile = Files.createTempFile("original_", ".pdf"); inputFile.transferTo(tempInputFile.toFile());
inputFile.transferTo(originalFile.toFile());
long inputFileSize = Files.size(originalFile);
Path currentFile = Files.createTempFile("working_", ".pdf"); long inputFileSize = Files.size(tempInputFile);
Files.copy(originalFile, currentFile, StandardCopyOption.REPLACE_EXISTING);
// Keep track of all temporary files for cleanup Path tempOutputFile = null;
List<Path> tempFiles = new ArrayList<>(); byte[] pdfBytes;
tempFiles.add(originalFile);
tempFiles.add(currentFile);
try { try {
tempOutputFile = Files.createTempFile("output_", ".pdf");
if (autoMode) { if (autoMode) {
double sizeReductionRatio = expectedOutputSize / (double) inputFileSize; double sizeReductionRatio = expectedOutputSize / (double) inputFileSize;
optimizeLevel = determineOptimizeLevel(sizeReductionRatio); optimizeLevel = determineOptimizeLevel(sizeReductionRatio);
} }
boolean sizeMet = false; boolean sizeMet = false;
boolean imageCompressionApplied = false; boolean grayscaleEnabled = Boolean.TRUE.equals(request.getGrayscale());
boolean qpdfCompressionApplied = false;
if (qpdfEnabled && optimizeLevel <= 3) {
optimizeLevel = 4;
}
while (!sizeMet && optimizeLevel <= 9) { 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 // Apply additional image compression for levels 6-9
Path compressedImageFile = if (optimizeLevel >= 6) {
compressImagesInPDF( // Calculate scale factor based on optimization level
currentFile, double scaleFactor =
scaleFactor, switch (optimizeLevel) {
jpegQuality, case 6 -> 0.9; // 90% of original size
Boolean.TRUE.equals(convertToGrayscale)); case 7 -> 0.8; // 80% of original size
case 8 -> 0.65; // 70% of original size
tempFiles.add(compressedImageFile); case 9 -> 0.5; // 60% of original size
currentFile = compressedImageFile; default -> 1.0;
imageCompressionApplied = true; };
compressImagesInPDF(tempInputFile, scaleFactor, grayscaleEnabled);
} }
// Apply QPDF compression for all levels // Run QPDF optimization
if (!qpdfCompressionApplied && qpdfEnabled) { List<String> command = new ArrayList<>();
applyQpdfCompression(request, optimizeLevel, currentFile, tempFiles); command.add("qpdf");
qpdfCompressionApplied = true; if (request.getNormalize()) {
} else if (!qpdfCompressionApplied) { command.add("--normalize-content=y");
// If QPDF is disabled, mark as applied and log }
if (!qpdfEnabled) { if (request.getLinearize()) {
log.info("Skipping QPDF compression as QPDF group is disabled"); command.add("--linearize");
}
command.add("--optimize-images");
command.add("--recompress-flate");
command.add("--compression-level=" + optimizeLevel);
command.add("--compress-streams=y");
command.add("--object-streams=generate");
command.add("--no-warn");
command.add(tempInputFile.toString());
command.add(tempOutputFile.toString());
ProcessExecutorResult returnCode = null;
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
} catch (Exception e) {
if (returnCode != null && returnCode.getRc() != 3) {
throw e;
} }
qpdfCompressionApplied = true;
} }
// Check if target size reached or not in auto mode // Check if file size is within expected size or not auto mode
long outputFileSize = Files.size(currentFile); long outputFileSize = Files.size(tempOutputFile);
if (outputFileSize <= expectedOutputSize || !autoMode) { if (outputFileSize <= expectedOutputSize || !autoMode) {
sizeMet = true; sizeMet = true;
} else { } else {
int newOptimizeLevel = optimizeLevel =
incrementOptimizeLevel( incrementOptimizeLevel(
optimizeLevel, outputFileSize, expectedOutputSize); optimizeLevel, outputFileSize, expectedOutputSize);
if (autoMode && optimizeLevel >= 9) {
// Check if we can't increase the level further log.info("Maximum compression level reached in auto mode");
if (newOptimizeLevel == optimizeLevel) { sizeMet = true;
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 // Read the optimized PDF file
long finalFileSize = Files.size(currentFile); pdfBytes = Files.readAllBytes(tempOutputFile);
if (finalFileSize >= inputFileSize) { Path finalFile = tempOutputFile;
// Check if optimized file is larger than the original
if (pdfBytes.length > inputFileSize) {
log.warn( log.warn(
"Optimized file is larger than the original. Using the original file instead."); "Optimized file is larger than the original. Returning the original file"
currentFile = originalFile; + " instead.");
finalFile = tempInputFile;
} }
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(inputFile.getOriginalFilename()) Filenames.toSimpleFileName(inputFile.getOriginalFilename())
.replaceFirst("[.][^.]+$", "") .replaceFirst("[.][^.]+$", "")
+ "_Optimized.pdf"; + "_Optimized.pdf";
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
pdfDocumentFactory.load(currentFile.toFile()), outputFilename); pdfDocumentFactory.load(finalFile.toFile()), outputFilename);
} finally { } finally {
// Clean up all temporary files Files.deleteIfExists(tempOutputFile);
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
List<String> command = new ArrayList<>();
command.add("qpdf");
if (request.getNormalize()) {
command.add("--normalize-content=y");
}
if (request.getLinearize()) {
command.add("--linearize");
}
command.add("--recompress-flate");
command.add("--compression-level=" + qpdfCompressionLevel);
command.add("--compress-streams=y");
command.add("--object-streams=generate");
command.add(currentFile.toString());
command.add(qpdfOutputFile.toString());
ProcessExecutorResult returnCode = null;
try {
returnCode =
ProcessExecutor.getInstance(ProcessExecutor.Processes.QPDF)
.runCommandWithOutputHandling(command);
// Update current file to the QPDF output
Files.copy(qpdfOutputFile, currentFile, StandardCopyOption.REPLACE_EXISTING);
long postQpdfSize = Files.size(currentFile);
double qpdfReduction = 100.0 - ((postQpdfSize * 100.0) / preQpdfSize);
log.info(
"Post-QPDF file size: {} (reduced by {}%)",
GeneralUtils.formatBytes(postQpdfSize), String.format("%.1f", qpdfReduction));
} catch (Exception e) {
if (returnCode != null && returnCode.getRc() != 3) {
throw new IOException("QPDF command failed", e);
}
// If QPDF fails, keep using the current file
log.warn("QPDF compression failed, continuing with current file", e);
}
}
// Pick optimization level based on target size
private int determineOptimizeLevel(double sizeReductionRatio) { private int determineOptimizeLevel(double sizeReductionRatio) {
if (sizeReductionRatio > 0.9) return 1; if (sizeReductionRatio > 0.9) return 1;
if (sizeReductionRatio > 0.8) return 2; if (sizeReductionRatio > 0.8) return 2;
if (sizeReductionRatio > 0.7) return 3; if (sizeReductionRatio > 0.7) return 3;
if (sizeReductionRatio > 0.6) return 4; if (sizeReductionRatio > 0.6) return 4;
if (sizeReductionRatio > 0.3) return 5; if (sizeReductionRatio > 0.5) return 5;
if (sizeReductionRatio > 0.2) return 6; if (sizeReductionRatio > 0.4) return 6;
if (sizeReductionRatio > 0.15) return 7; if (sizeReductionRatio > 0.3) return 7;
if (sizeReductionRatio > 0.1) return 8; if (sizeReductionRatio > 0.2) return 8;
return 9; return 9;
} }
// Increment optimization level if we need more compression
private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) { private int incrementOptimizeLevel(int currentLevel, long currentSize, long targetSize) {
double currentRatio = currentSize / (double) targetSize; double currentRatio = currentSize / (double) targetSize;
log.info("Current compression ratio: {}", String.format("%.2f", currentRatio)); 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 javax.imageio.ImageIO;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest; 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.CheckProgramInstall;
import stirling.software.SPDF.utils.ProcessExecutor; import stirling.software.SPDF.utils.ProcessExecutor;
import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult; import stirling.software.SPDF.utils.ProcessExecutor.ProcessExecutorResult;
@ -46,21 +45,11 @@ public class ExtractImageScansController {
private static final String REPLACEFIRST = "[.][^.]+$"; 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") @PostMapping(consumes = "multipart/form-data", value = "/extract-image-scans")
@Operation( @Operation(
summary = "Extract image scans from an input file", summary = "Extract image scans from an input file",
description = description =
"This endpoint extracts image scans from a given file based on certain" "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")
+ " 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( public ResponseEntity<byte[]> extractImageScans(
@RequestBody( @RequestBody(
description = "Form data containing file and extraction parameters", description = "Form data containing file and extraction parameters",
@ -98,7 +87,7 @@ public class ExtractImageScansController {
// Check if input file is a PDF // Check if input file is a PDF
if ("pdf".equalsIgnoreCase(extension)) { if ("pdf".equalsIgnoreCase(extension)) {
// Load PDF document // Load PDF document
try (PDDocument document = pdfDocumentFactory.load(form.getFileInput())) { try (PDDocument document = Loader.loadPDF(form.getFileInput().getBytes())) {
PDFRenderer pdfRenderer = new PDFRenderer(document); PDFRenderer pdfRenderer = new PDFRenderer(document);
pdfRenderer.setSubsamplingAllowed(true); pdfRenderer.setSubsamplingAllowed(true);
int pageCount = document.getNumberOfPages(); int pageCount = document.getNumberOfPages();

View File

@ -20,11 +20,11 @@ import java.util.zip.ZipOutputStream;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ModelAttribute; 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 lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.PDFExtractImagesRequest; import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
import stirling.software.SPDF.service.CustomPDFDocumentFactory;
import stirling.software.SPDF.utils.ImageProcessingUtils; import stirling.software.SPDF.utils.ImageProcessingUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@ -50,26 +49,17 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class ExtractImagesController { public class ExtractImagesController {
private final CustomPDFDocumentFactory pdfDocumentFactory;
@Autowired
public ExtractImagesController(CustomPDFDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory;
}
@PostMapping(consumes = "multipart/form-data", value = "/extract-images") @PostMapping(consumes = "multipart/form-data", value = "/extract-images")
@Operation( @Operation(
summary = "Extract images from a PDF file", summary = "Extract images from a PDF file",
description = description =
"This endpoint extracts images from a given PDF file and returns them in a zip" "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")
+ " file. Users can specify the output image format. Input:PDF"
+ " Output:IMAGE/ZIP Type:SIMO")
public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request) public ResponseEntity<byte[]> extractImages(@ModelAttribute PDFExtractImagesRequest request)
throws IOException, InterruptedException, ExecutionException { throws IOException, InterruptedException, ExecutionException {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
String format = request.getFormat(); String format = request.getFormat();
boolean allowDuplicates = request.isAllowDuplicates(); 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 // Determine if multithreading should be used based on PDF size or number of pages
boolean useMultithreading = shouldUseMultithreading(file, document); 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.awt.image.BufferedImage;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
@ -26,7 +27,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.model.api.misc.FlattenRequest; 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; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@ -35,10 +36,10 @@ import stirling.software.SPDF.utils.WebResponseUtils;
@Tag(name = "Misc", description = "Miscellaneous APIs") @Tag(name = "Misc", description = "Miscellaneous APIs")
public class FlattenController { public class FlattenController {
private final CustomPDFDocumentFactory pdfDocumentFactory; private final CustomPDDocumentFactory pdfDocumentFactory;
@Autowired @Autowired
public FlattenController(CustomPDFDocumentFactory pdfDocumentFactory) { public FlattenController(CustomPDDocumentFactory pdfDocumentFactory) {
this.pdfDocumentFactory = pdfDocumentFactory; this.pdfDocumentFactory = pdfDocumentFactory;
} }
@ -46,12 +47,11 @@ public class FlattenController {
@Operation( @Operation(
summary = "Flatten PDF form fields or full page", summary = "Flatten PDF form fields or full page",
description = description =
"Flattening just PDF form fields or converting each page to images to make text" "Flattening just PDF form fields or converting each page to images to make text unselectable. Input:PDF, Output:PDF. Type:SISO")
+ " unselectable. Input:PDF, Output:PDF. Type:SISO")
public ResponseEntity<byte[]> flatten(@ModelAttribute FlattenRequest request) throws Exception { public ResponseEntity<byte[]> flatten(@ModelAttribute FlattenRequest request) throws Exception {
MultipartFile file = request.getFileInput(); MultipartFile file = request.getFileInput();
PDDocument document = pdfDocumentFactory.load(file); PDDocument document = Loader.loadPDF(file.getBytes());
Boolean flattenOnlyForms = request.getFlattenOnlyForms(); Boolean flattenOnlyForms = request.getFlattenOnlyForms();
if (Boolean.TRUE.equals(flattenOnlyForms)) { if (Boolean.TRUE.equals(flattenOnlyForms)) {

View File

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

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