mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89db09a4e6 |
@@ -1,7 +1,7 @@
|
||||
name: Backend build, format check, and coverage
|
||||
|
||||
# Reusable workflow called from build.yml. Runs the backend build matrix
|
||||
# (JDK 25 × spring-security on/off), Spotless formatting check, JUnit, and
|
||||
# Reusable workflow called from build.yml. Runs the full backend build matrix
|
||||
# (JDK 21/25 × spring-security on/off), Spotless formatting check, JUnit, and
|
||||
# posts Jacoco coverage to PRs.
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
jdk-version: [25]
|
||||
jdk-version: [21, 25]
|
||||
spring-security: [true, false]
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
|
||||
@@ -21,6 +21,6 @@ jobs:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: "Dependency Review"
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
|
||||
with:
|
||||
config-file: "./.github/config/dependency-review-config.yml"
|
||||
|
||||
@@ -90,21 +90,21 @@ jobs:
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For push/release events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}]}' >> $GITHUB_OUTPUT
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-jars:
|
||||
@@ -256,17 +256,15 @@ jobs:
|
||||
if: matrix.platform == 'macos-15'
|
||||
env:
|
||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:jlink:universal-mac
|
||||
|
||||
- name: Prepare desktop build
|
||||
run: task desktop:prepare
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:prepare
|
||||
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
@@ -640,7 +638,7 @@ jobs:
|
||||
run: ls -R ./artifacts
|
||||
|
||||
- name: Upload binaries to Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
||||
with:
|
||||
tag_name: v${{ needs.determine-matrix.outputs.version }}
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
sbom: true
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
|
||||
@@ -96,13 +96,13 @@ jobs:
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
- name: Install cosign
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/V2-master'
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
with:
|
||||
cosign-release: "v2.4.1"
|
||||
|
||||
|
||||
@@ -75,6 +75,6 @@ jobs:
|
||||
|
||||
# Upload the results to GitHub's code scanning dashboard.
|
||||
- name: "Upload to code-scanning"
|
||||
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v3.29.5
|
||||
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v3.29.5
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -47,10 +47,12 @@ jobs:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
PLATFORM: ${{ inputs.platform }}
|
||||
run: |
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64","jpdfium_platforms":"windows-x64"}'
|
||||
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}'
|
||||
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}'
|
||||
MACOS='{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"}'
|
||||
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}'
|
||||
|
||||
# Resolve requested platform — populated by either workflow_dispatch
|
||||
# or workflow_call inputs; both paths default to "all".
|
||||
case "$PLATFORM" in
|
||||
windows) ENTRIES=("$WINDOWS") ;;
|
||||
macos) ENTRIES=("$MACOS") ;;
|
||||
@@ -110,6 +112,10 @@ jobs:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
# x86_64 JDK is set up first so the aarch64 step below can leave its
|
||||
# JAVA_HOME as the active one. The macOS universal JRE build needs
|
||||
# jmods from both arches; the x64 path is captured into the env
|
||||
# before the second setup-java overwrites JAVA_HOME.
|
||||
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||
if: matrix.platform == 'macos-15'
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
@@ -136,21 +142,21 @@ jobs:
|
||||
- name: Setup Task
|
||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
||||
|
||||
# Build the universal JRE before desktop:prepare so the jlink:runtime
|
||||
# task short-circuits on its `test -d runtime/jre` status check.
|
||||
- name: Build universal macOS JRE
|
||||
if: matrix.platform == 'macos-15'
|
||||
env:
|
||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:jlink:universal-mac
|
||||
|
||||
- name: Prepare desktop build
|
||||
run: task desktop:prepare
|
||||
env:
|
||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||
run: task desktop:prepare
|
||||
|
||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||
- name: Setup DigiCert KeyLocker
|
||||
@@ -263,10 +269,6 @@ jobs:
|
||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported successfully."
|
||||
|
||||
- name: Sign JPDFium dylibs inside bootJar (macOS only)
|
||||
if: matrix.platform == 'macos-15' && env.APPLE_CERTIFICATE != ''
|
||||
run: bash frontend/scripts/sign-jpdfium-dylibs-in-bootjar.sh
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-15'
|
||||
run: |
|
||||
|
||||
@@ -165,7 +165,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
||||
uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
|
||||
@@ -265,8 +265,6 @@ docs/type3/signatures/
|
||||
# Type3 sample PDFs (development only)
|
||||
**/type3/samples/
|
||||
|
||||
**/application-dev-local.properties
|
||||
|
||||
# Claude
|
||||
.claude/
|
||||
|
||||
|
||||
+15
-66
@@ -1,19 +1,7 @@
|
||||
version: '3'
|
||||
|
||||
# Gradle invocation strategy:
|
||||
# - Linux/macOS: `./gradlew` runs the POSIX shell wrapper natively.
|
||||
# - Windows: `cmd /c ".\gradlew.bat ..."` invokes the .bat wrapper through
|
||||
# cmd.exe so it inherits the user's Windows-side `JAVA_HOME`
|
||||
# and PATH. Routing through `bash gradlew` on Windows ends up
|
||||
# under WSL or Git-Bash, neither of which inherits
|
||||
# Adoptium/Temurin's default Windows-only Java env - gradlew
|
||||
# then errors with "JAVA_HOME is not set and no 'java' command
|
||||
# could be found".
|
||||
#
|
||||
# The entire `.\gradlew.bat ...` payload is double-quoted so
|
||||
# the leading `.\` survives mvdan/sh's POSIX backslash
|
||||
# stripping; cmd.exe also requires `.\` (not bare `gradlew.bat`)
|
||||
# because modern Windows excludes cwd from cmd's search path.
|
||||
# All gradle invocations go through `bash gradlew` (the sh wrapper) so they work
|
||||
# uniformly on Linux/macOS (native bash) and Windows-with-Git-Bash.
|
||||
|
||||
tasks:
|
||||
dev:
|
||||
@@ -25,67 +13,43 @@ tasks:
|
||||
env:
|
||||
SERVER_PORT: '{{.PORT}}'
|
||||
cmds:
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}cmd /c ".\gradlew.bat :stirling-pdf:bootRun"'
|
||||
platforms: [windows]
|
||||
- cmd: '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}./gradlew :stirling-pdf:bootRun'
|
||||
platforms: [linux, darwin]
|
||||
- '{{if .AIENGINE_URL}}AIENGINE_URL={{.AIENGINE_URL}} AIENGINE_ENABLED=true {{end}}bash gradlew :stirling-pdf:bootRun'
|
||||
|
||||
dev:bundled:
|
||||
desc: "Clean + bootRun with frontend bundled into the backend (single :8080 server)"
|
||||
ignore_error: true
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean bootRun -PbuildWithFrontend=true"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean bootRun -PbuildWithFrontend=true
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean bootRun -PbuildWithFrontend=true
|
||||
|
||||
build:
|
||||
desc: "Full backend build"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean build"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean build
|
||||
|
||||
build:fast:
|
||||
desc: "Build without tests"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean build -x test"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean build -x test
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean build -x test
|
||||
|
||||
build:ci:
|
||||
desc: "Build for CI (formatting checked separately)"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat build -PnoSpotless"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew build -PnoSpotless
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew build -PnoSpotless
|
||||
|
||||
test:
|
||||
desc: "Run backend tests"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat test"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew test
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew test
|
||||
|
||||
format:
|
||||
desc: "Auto-fix code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat spotlessApply"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessApply
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew spotlessApply
|
||||
|
||||
format:check:
|
||||
desc: "Check code formatting"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat spotlessCheck"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew spotlessCheck
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew spotlessCheck
|
||||
|
||||
fix:
|
||||
desc: "Auto-fix backend"
|
||||
@@ -95,10 +59,7 @@ tasks:
|
||||
swagger:
|
||||
desc: "Generate OpenAPI docs"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat :stirling-pdf:copySwaggerDoc"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew :stirling-pdf:copySwaggerDoc
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew :stirling-pdf:copySwaggerDoc
|
||||
sources:
|
||||
- app/core/src/main/java/**/*.java
|
||||
- app/proprietary/src/main/java/**/*.java
|
||||
@@ -116,31 +77,19 @@ tasks:
|
||||
desc: "Print project version"
|
||||
silent: true
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat printVersion --quiet" | tail -1
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew printVersion --quiet | tail -1
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew printVersion --quiet | tail -1
|
||||
|
||||
licenses:check:
|
||||
desc: "Check dependency licenses"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat checkLicense --no-parallel"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew checkLicense --no-parallel
|
||||
|
||||
licenses:generate:
|
||||
desc: "Check and generate dependency license report"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat checkLicense generateLicenseReport --no-parallel"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew checkLicense generateLicenseReport --no-parallel
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew checkLicense generateLicenseReport --no-parallel
|
||||
|
||||
clean:
|
||||
desc: "Clean build artifacts"
|
||||
cmds:
|
||||
- cmd: cmd /c ".\gradlew.bat clean"
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew clean
|
||||
platforms: [linux, darwin]
|
||||
- bash gradlew clean
|
||||
|
||||
+3
-20
@@ -3,22 +3,6 @@ version: '3'
|
||||
vars:
|
||||
JLINK_MODULES: "java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
|
||||
# Override via JPDFIUM_PLATFORMS env (csv of platform keys, or 'all').
|
||||
JPDFIUM_PLATFORMS:
|
||||
sh: |
|
||||
if [ -n "${JPDFIUM_PLATFORMS:-}" ]; then
|
||||
echo "$JPDFIUM_PLATFORMS"
|
||||
else
|
||||
case "{{OS}}-{{ARCH}}" in
|
||||
darwin-arm64) echo "darwin-arm64";;
|
||||
darwin-amd64) echo "darwin-x64";;
|
||||
linux-amd64) echo "linux-x64";;
|
||||
linux-arm64) echo "linux-arm64";;
|
||||
windows-amd64) echo "windows-x64";;
|
||||
*) echo "all";;
|
||||
esac
|
||||
fi
|
||||
|
||||
tasks:
|
||||
prepare:
|
||||
desc: "Prepare desktop build dependencies"
|
||||
@@ -87,16 +71,15 @@ tasks:
|
||||
deps: [jlink:jar, jlink:runtime]
|
||||
|
||||
jlink:jar:
|
||||
desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)"
|
||||
desc: "Build backend JAR for Tauri bundling"
|
||||
run: once
|
||||
dir: ..
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||
cmds:
|
||||
- echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"
|
||||
- cmd: cmd /c gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
- cmd: cmd /c gradlew.bat bootJar --no-daemon
|
||||
platforms: [windows]
|
||||
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||
- cmd: ./gradlew bootJar --no-daemon
|
||||
platforms: [linux, darwin]
|
||||
- mkdir -p frontend/src-tauri/libs
|
||||
- cp app/core/build/libs/stirling-pdf-*.jar frontend/src-tauri/libs/
|
||||
|
||||
@@ -431,7 +431,7 @@ The frontend is organized with a clear separation of concerns:
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Java Version**: Requires JDK 25.
|
||||
- **Java Version**: Minimum JDK 21, supports and recommends JDK 25
|
||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||
- **File Persistence**:
|
||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
**Stirling 2.0** is built using:
|
||||
|
||||
**Backend:**
|
||||
- Spring Boot (requires JDK 25)
|
||||
- Spring Boot (Java 21+, JDK 25 recommended)
|
||||
- PDFBox for core PDF operations
|
||||
- LibreOffice for document conversions
|
||||
- qpdf for PDF optimization
|
||||
@@ -45,7 +45,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
- [Task](https://taskfile.dev/installation/) — unified command runner (recommended)
|
||||
- Docker
|
||||
- Git
|
||||
- Java JDK 25
|
||||
- Java JDK 21 or later (JDK 25 recommended)
|
||||
- Node.js 18+ and npm (required for frontend development)
|
||||
- Gradle 7.0 or later (Included within the repo)
|
||||
- [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development)
|
||||
@@ -61,7 +61,7 @@ This guide focuses on developing for Stirling 2.0, including both the React fron
|
||||
cd Stirling-PDF
|
||||
```
|
||||
|
||||
2. Install Docker and JDK 25 if not already installed.
|
||||
2. Install Docker and JDK 21 (or JDK 25 recommended) if not already installed.
|
||||
|
||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||
1. Only VSCode
|
||||
|
||||
@@ -6,8 +6,6 @@ Portions of this software are licensed as follows:
|
||||
|
||||
* All content that resides under the "app/proprietary/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "app/proprietary/LICENSE".
|
||||
* All content that resides under the "app/saas/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "app/saas/LICENSE".
|
||||
* All content that resides under the "engine/" directory of this repository,
|
||||
if that directory exists, is licensed under the license defined in "engine/LICENSE".
|
||||
* All content that resides under the "frontend/src/proprietary/" directory of this repository,
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ version: '3'
|
||||
output: prefixed
|
||||
|
||||
vars:
|
||||
FIND_FREE_PORT_PS: powershell -NoProfile -File scripts/find-free-port.ps1
|
||||
FIND_FREE_PORT_PS: powershell -NoProfile -File scripts\find-free-port.ps1 -Preferred
|
||||
FIND_FREE_PORT_SH: bash scripts/find-free-port.sh
|
||||
|
||||
includes:
|
||||
@@ -45,7 +45,7 @@ tasks:
|
||||
desc: "Start backend + frontend concurrently on free ports"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
deps:
|
||||
@@ -62,7 +62,7 @@ tasks:
|
||||
desc: "Start backend + frontend + engine concurrently on free ports"
|
||||
vars:
|
||||
PORTS:
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080 5173 5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
|
||||
sh: '{{if eq OS "windows"}}{{.FIND_FREE_PORT_PS}} 8080,5173,5001{{else}}{{.FIND_FREE_PORT_SH}} 8080 5173 5001{{end}}'
|
||||
BACKEND_PORT: '{{index (splitList "\n" .PORTS) 0}}'
|
||||
FRONTEND_PORT: '{{index (splitList "\n" .PORTS) 1}}'
|
||||
ENGINE_PORT: '{{index (splitList "\n" .PORTS) 2}}'
|
||||
|
||||
@@ -59,25 +59,4 @@ dependencies {
|
||||
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
|
||||
exclude group: 'com.google.code.gson', module: 'gson'
|
||||
}
|
||||
|
||||
api 'com.stirling:jpdfium:1.0.0'
|
||||
|
||||
// -PjpdfiumPlatforms=all|<csv of linux-x64,linux-arm64,darwin-x64,darwin-arm64,windows-x64>
|
||||
def jpdfiumPlatformsProp = (project.findProperty('jpdfiumPlatforms') ?: 'all').toString().trim()
|
||||
def jpdfiumAllPlatforms = ['linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64', 'windows-x64']
|
||||
def jpdfiumPlatforms = jpdfiumPlatformsProp == 'all'
|
||||
? jpdfiumAllPlatforms
|
||||
: jpdfiumPlatformsProp.split(',').collect { it.trim() }.findAll { it }
|
||||
def jpdfiumInvalid = jpdfiumPlatforms.findAll { !jpdfiumAllPlatforms.contains(it) }
|
||||
if (jpdfiumInvalid) {
|
||||
throw new GradleException("Unknown jpdfiumPlatforms value(s): ${jpdfiumInvalid.join(', ')}. " +
|
||||
"Valid: ${jpdfiumAllPlatforms.join(', ')} or 'all'.")
|
||||
}
|
||||
logger.lifecycle("JPDFium native platforms: ${jpdfiumPlatforms.join(', ')}")
|
||||
jpdfiumPlatforms.each { platform ->
|
||||
runtimeOnly "com.stirling:jpdfium-natives-${platform}:1.0.0"
|
||||
}
|
||||
|
||||
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
|
||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
|
||||
}
|
||||
|
||||
+3
-3
@@ -75,8 +75,8 @@ public @interface AutoJobPostMapping {
|
||||
boolean queueable() default false;
|
||||
|
||||
/**
|
||||
* Relative resource weight (1-100). See {@link
|
||||
* stirling.software.common.enumeration.ResourceWeight} for the standard tiers.
|
||||
* Relative resource weight (1–100) used by the scheduler to prioritise / throttle jobs. Values
|
||||
* below 1 are clamped to 1, values above 100 to 100.
|
||||
*/
|
||||
int resourceWeight() default 1;
|
||||
int resourceWeight() default 50;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package stirling.software.common.enumeration;
|
||||
|
||||
/**
|
||||
* Standard resource-weight tiers for {@link
|
||||
* stirling.software.common.annotations.AutoJobPostMapping#resourceWeight()}.
|
||||
*/
|
||||
public final class ResourceWeight {
|
||||
|
||||
/** Lightweight: rotate, page numbers, extract pages, permissions, etc. */
|
||||
public static final int SMALL_WEIGHT = 1;
|
||||
|
||||
/** Medium: merge, split, multi-tool batch. */
|
||||
public static final int MEDIUM_WEIGHT = 3;
|
||||
|
||||
/** Heavy: compress, OCR (small), conversions, raster. */
|
||||
public static final int LARGE_WEIGHT = 5;
|
||||
|
||||
/** Extra heavy: OCR on large files, full-document re-render, AI-assisted edits. */
|
||||
public static final int XLARGE_WEIGHT = 10;
|
||||
|
||||
private ResourceWeight() {}
|
||||
}
|
||||
@@ -97,14 +97,7 @@ public class ApplicationProperties {
|
||||
EncodedResource encodedResource = new EncodedResource(resource);
|
||||
PropertySource<?> propertySource =
|
||||
new YamlPropertySourceFactory().createPropertySource(null, encodedResource);
|
||||
|
||||
boolean saasActive = Arrays.asList(environment.getActiveProfiles()).contains("saas");
|
||||
if (saasActive) {
|
||||
// Saas-pinned values in application-saas.properties must beat settings.yml.
|
||||
environment.getPropertySources().addLast(propertySource);
|
||||
} else {
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
|
||||
log.debug("Loaded properties: {}", propertySource.getSource());
|
||||
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
package stirling.software.common.model.enumeration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/** Team invitation status */
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum InvitationStatus {
|
||||
PENDING("PENDING"),
|
||||
ACCEPTED("ACCEPTED"),
|
||||
REJECTED("REJECTED"),
|
||||
CANCELLED("CANCELLED"),
|
||||
EXPIRED("EXPIRED");
|
||||
|
||||
private final String statusName;
|
||||
|
||||
public static InvitationStatus fromString(String statusName) {
|
||||
for (InvitationStatus status : InvitationStatus.values()) {
|
||||
if (status.getStatusName().equalsIgnoreCase(statusName)) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No InvitationStatus defined for name: " + statusName);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ public enum Role {
|
||||
// Unlimited access
|
||||
USER("ROLE_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.user"),
|
||||
|
||||
// Paid tier; set by Stripe webhooks under saas mode.
|
||||
PRO_USER("ROLE_PRO_USER", Integer.MAX_VALUE, Integer.MAX_VALUE, "adminUserSettings.proUser"),
|
||||
|
||||
// 40 API calls Per Day, 40 web calls
|
||||
LIMITED_API_USER("ROLE_LIMITED_API_USER", 40, 40, "adminUserSettings.apiUser"),
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package stirling.software.common.model.enumeration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Team membership roles LEADER: Can invite/remove members, manage settings, view usage, manage
|
||||
* billing MEMBER: Regular team member with standard access
|
||||
*/
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum TeamRole {
|
||||
LEADER("LEADER"),
|
||||
MEMBER("MEMBER");
|
||||
|
||||
private final String roleName;
|
||||
|
||||
public static TeamRole fromString(String roleName) {
|
||||
for (TeamRole role : TeamRole.values()) {
|
||||
if (role.getRoleName().equalsIgnoreCase(roleName)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No TeamRole defined for name: " + roleName);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package stirling.software.common.architecture;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
/**
|
||||
* Module dependency-direction guardrails. Allowed direction: {@code saas → proprietary → common}
|
||||
* and {@code stirling-pdf → proprietary → common}.
|
||||
*/
|
||||
class ArchitectureTest {
|
||||
|
||||
private static final JavaClasses commonClasses =
|
||||
new ClassFileImporter()
|
||||
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
|
||||
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
|
||||
.importPackages("stirling.software.common");
|
||||
|
||||
@Test
|
||||
void commonDoesNotDependOnCore() {
|
||||
ArchRule rule =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("stirling.software.common..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("stirling.software.SPDF..");
|
||||
rule.check(commonClasses);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonDoesNotDependOnProprietary() {
|
||||
ArchRule rule =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("stirling.software.common..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("stirling.software.proprietary..");
|
||||
rule.check(commonClasses);
|
||||
}
|
||||
|
||||
@Test
|
||||
void commonDoesNotDependOnSaas() {
|
||||
ArchRule rule =
|
||||
noClasses()
|
||||
.that()
|
||||
.resideInAPackage("stirling.software.common..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("stirling.software.saas..");
|
||||
rule.check(commonClasses);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package stirling.software.common.jpdfium;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
|
||||
class JPDFiumSmokeTest {
|
||||
|
||||
@Test
|
||||
void opensExamplePdfAndReadsPageCount(@TempDir Path tmp) throws IOException {
|
||||
Path pdf = tmp.resolve("example.pdf");
|
||||
try (InputStream in = getClass().getResourceAsStream("/example.pdf")) {
|
||||
assertNotNull(in, "example.pdf must exist under src/test/resources");
|
||||
Files.copy(in, pdf);
|
||||
}
|
||||
|
||||
try (PdfDocument doc = PdfDocument.open(pdf)) {
|
||||
assertTrue(
|
||||
doc.pageCount() >= 1,
|
||||
"PdfDocument should report at least one page for example.pdf");
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-45
@@ -39,14 +39,12 @@ spotless {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
if (!gradle.ext.disableAdditional) {
|
||||
if (System.getenv('DISABLE_ADDITIONAL_FEATURES') != 'true'
|
||||
|| (project.hasProperty('DISABLE_ADDITIONAL_FEATURES')
|
||||
&& System.getProperty('DISABLE_ADDITIONAL_FEATURES') != 'true')) {
|
||||
implementation project(':proprietary')
|
||||
}
|
||||
|
||||
if (gradle.ext.enableSaas) {
|
||||
implementation project(':saas')
|
||||
}
|
||||
|
||||
implementation project(':common')
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
implementation 'org.eclipse.jetty.http2:jetty-http2-server'
|
||||
@@ -136,17 +134,6 @@ sourceSets {
|
||||
|
||||
}
|
||||
|
||||
// Forward bench-related system properties through to the test JVM so
|
||||
// `-Dsplit.bench=true` (and similar) flags reach Boolean.getBoolean().
|
||||
tasks.named('test', Test) {
|
||||
['split.bench', 'split.bench.pages', 'split.bench.chunk',
|
||||
'split.bench.imgW', 'split.bench.imgH'].each { key ->
|
||||
def v = System.getProperty(key)
|
||||
if (v != null) systemProperty(key, v)
|
||||
}
|
||||
maxHeapSize = '2g'
|
||||
}
|
||||
|
||||
|
||||
// Disable regular jar
|
||||
jar {
|
||||
@@ -173,8 +160,7 @@ bootJar {
|
||||
manifest {
|
||||
attributes(
|
||||
'Implementation-Title': 'Stirling-PDF',
|
||||
'Implementation-Version': project.version,
|
||||
'Enable-Native-Access': 'ALL-UNNAMED'
|
||||
'Implementation-Version': project.version
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -187,23 +173,6 @@ springBoot {
|
||||
// Frontend build tasks - only enabled with -PbuildWithFrontend=true
|
||||
def buildWithFrontend = project.hasProperty('buildWithFrontend') && project.property('buildWithFrontend') == 'true'
|
||||
def buildPrototypes = project.hasProperty('prototypesMode') && project.property('prototypesMode') == 'true'
|
||||
|
||||
// Vite mode: -PprototypesMode > -PfrontendMode > enableSaas > disableAdditional > proprietary.
|
||||
def frontendModeOverride = project.findProperty('frontendMode')?.toString()?.toLowerCase()
|
||||
def frontendMode
|
||||
if (buildPrototypes) {
|
||||
frontendMode = 'prototypes'
|
||||
} else if (frontendModeOverride) {
|
||||
frontendMode = frontendModeOverride
|
||||
} else if (gradle.ext.enableSaas) {
|
||||
frontendMode = 'saas'
|
||||
} else if (gradle.ext.disableAdditional) {
|
||||
frontendMode = 'core'
|
||||
} else {
|
||||
frontendMode = 'proprietary'
|
||||
}
|
||||
def frontendBuildTask = "frontend:build:${frontendMode}"
|
||||
|
||||
def frontendDir = file('../../frontend')
|
||||
def frontendDistDir = file('../../frontend/dist')
|
||||
def resourcesStaticDir = file('src/main/resources/static')
|
||||
@@ -272,7 +241,7 @@ tasks.register('npmBuild', Exec) {
|
||||
group = 'frontend'
|
||||
description = 'Build frontend application'
|
||||
workingDir file('../..')
|
||||
commandLine = ['task', frontendBuildTask]
|
||||
commandLine = buildPrototypes ? ['task', 'frontend:build:prototypes'] : ['task', 'frontend:build']
|
||||
inputs.dir(new File(frontendDir, 'src'))
|
||||
inputs.dir(new File(frontendDir, 'public'))
|
||||
inputs.file(new File(frontendDir, 'package.json'))
|
||||
@@ -287,7 +256,7 @@ tasks.register('npmBuild', Exec) {
|
||||
environment 'VITE_API_BASE_URL', '/'
|
||||
|
||||
doFirst {
|
||||
println "Building frontend application for production (mode=${frontendMode}, VITE_API_BASE_URL=/)"
|
||||
println "Building frontend application for production (VITE_API_BASE_URL=/)"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +265,6 @@ tasks.register('copyFrontendAssets', Copy) {
|
||||
group = 'frontend'
|
||||
description = 'Copy frontend build to static resources'
|
||||
dependsOn npmBuild
|
||||
dependsOn cleanFrontendAssets
|
||||
from(frontendDistDir) {
|
||||
// Exclude files that conflict with backend static resources
|
||||
exclude 'robots.txt' // Backend already has this
|
||||
@@ -337,7 +305,7 @@ tasks.named('copyFrontendAssets').configure {
|
||||
}
|
||||
|
||||
if (buildWithFrontend) {
|
||||
println "Frontend build enabled - JAR will include React frontend (mode=${frontendMode})"
|
||||
println "Frontend build enabled - JAR will include React frontend"
|
||||
processResources.dependsOn copyFrontendAssets
|
||||
} else {
|
||||
println "Frontend build disabled - JAR will be backend-only with API landing page"
|
||||
@@ -346,9 +314,4 @@ if (buildWithFrontend) {
|
||||
}
|
||||
|
||||
bootJar.dependsOn ':common:jar'
|
||||
if (!gradle.ext.disableAdditional) {
|
||||
bootJar.dependsOn ':proprietary:jar'
|
||||
}
|
||||
if (gradle.ext.enableSaas) {
|
||||
bootJar.dependsOn ':saas:jar'
|
||||
}
|
||||
bootJar.dependsOn ':proprietary:jar'
|
||||
|
||||
@@ -35,8 +35,7 @@ import stirling.software.common.model.ApplicationProperties;
|
||||
scanBasePackages = {
|
||||
"stirling.software.SPDF",
|
||||
"stirling.software.common",
|
||||
"stirling.software.proprietary",
|
||||
"stirling.software.saas"
|
||||
"stirling.software.proprietary"
|
||||
})
|
||||
public class SPDFApplication {
|
||||
|
||||
@@ -206,22 +205,15 @@ public class SPDFApplication {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Detect classpath shape and pick the matching profile chain.
|
||||
boolean hasSaas = isClassPresent("stirling.software.saas.security.SupabaseSecurityConfig");
|
||||
boolean hasSecurity =
|
||||
isClassPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration");
|
||||
|
||||
if (hasSaas) {
|
||||
log.info("SaaS features in jar");
|
||||
return new String[] {"security", "saas"};
|
||||
}
|
||||
if (hasSecurity) {
|
||||
// 2. Detect if SecurityConfiguration is present on classpath
|
||||
if (isClassPresent(
|
||||
"stirling.software.proprietary.security.configuration.SecurityConfiguration")) {
|
||||
log.info("Additional features in jar");
|
||||
return new String[] {"security"};
|
||||
} else {
|
||||
log.info("Without additional features in jar");
|
||||
return new String[] {"default"};
|
||||
}
|
||||
log.info("Without additional features in jar");
|
||||
return new String[] {"default"};
|
||||
}
|
||||
|
||||
private static boolean isClassPresent(String className) {
|
||||
|
||||
+8
-31
@@ -23,7 +23,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.config.swagger.JsonDataResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.AnalysisApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
|
||||
@@ -33,10 +32,7 @@ public class AnalysisController {
|
||||
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/page-count",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/page-count", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF page count",
|
||||
@@ -47,10 +43,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/basic-info",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/basic-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get basic PDF information",
|
||||
@@ -67,8 +60,7 @@ public class AnalysisController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/document-properties",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get PDF document properties",
|
||||
@@ -98,10 +90,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/page-dimensions",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/page-dimensions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get page dimensions for all pages",
|
||||
@@ -121,10 +110,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/form-fields",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/form-fields", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get form field information",
|
||||
@@ -148,10 +134,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/annotation-info",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/annotation-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get annotation information",
|
||||
@@ -176,10 +159,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/font-info",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/font-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get font information",
|
||||
@@ -205,10 +185,7 @@ public class AnalysisController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/security-info",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/security-info", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@JsonDataResponse
|
||||
@Operation(
|
||||
summary = "Get security information",
|
||||
|
||||
+1
-3
@@ -28,7 +28,6 @@ import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.SPDF.model.api.general.BookletImpositionRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -45,8 +44,7 @@ public class BookletImpositionController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/booklet-imposition",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Create a booklet with proper page imposition",
|
||||
description =
|
||||
|
||||
@@ -26,7 +26,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.general.CropPdfForm;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -127,10 +126,7 @@ public class CropController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/crop",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/crop", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Crops a PDF document",
|
||||
description =
|
||||
|
||||
+2
-5
@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.EditTableOfContentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -45,8 +44,7 @@ public class EditTableOfContentsController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/extract-bookmarks",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Extract PDF Bookmarks",
|
||||
description = "Extracts bookmarks/table of contents from a PDF document as JSON.")
|
||||
@@ -149,8 +147,7 @@ public class EditTableOfContentsController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/edit-table-of-contents",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Edit Table of Contents",
|
||||
description = "Add or edit bookmarks/table of contents in a PDF document.")
|
||||
|
||||
+65
-171
@@ -3,14 +3,13 @@ package stirling.software.SPDF.controller.api;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||
@@ -40,7 +39,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.MergePdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -48,11 +46,6 @@ import stirling.software.common.util.PdfErrorUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfMerge;
|
||||
import stirling.software.jpdfium.doc.Bookmark;
|
||||
import stirling.software.jpdfium.doc.PdfBookmarkEditor;
|
||||
import stirling.software.jpdfium.doc.PdfBookmarkEditor.BookmarkTree;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@@ -63,6 +56,7 @@ public class MergeController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
// Merges a list of PDDocument objects into a single PDDocument
|
||||
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||
PDDocument mergedDoc = pdfDocumentFactory.createNewDocument();
|
||||
boolean success = false;
|
||||
@@ -81,8 +75,11 @@ public class MergeController {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-order files to match the explicit order provided by the front-end.
|
||||
// fileOrder is newline-delimited original filenames in the desired order.
|
||||
private static MultipartFile[] reorderFilesByProvidedOrder(
|
||||
MultipartFile[] files, String fileOrder) {
|
||||
// Split by various line endings and trim each entry
|
||||
String[] desired =
|
||||
stirling.software.common.util.RegexPatternUtils.getInstance()
|
||||
.getNewlineSplitPattern()
|
||||
@@ -109,6 +106,7 @@ public class MergeController {
|
||||
return ordered.toArray(new MultipartFile[0]);
|
||||
}
|
||||
|
||||
// Returns a comparator for sorting MultipartFile arrays based on the given sort type
|
||||
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
||||
return switch (sortType) {
|
||||
case "byFileName" ->
|
||||
@@ -156,16 +154,18 @@ public class MergeController {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
case "orderProvided" -> (file1, file2) -> 0;
|
||||
default -> (file1, file2) -> 0;
|
||||
case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided
|
||||
default -> (file1, file2) -> 0; // Default is the order provided
|
||||
};
|
||||
}
|
||||
|
||||
// Parse client file IDs from JSON string
|
||||
private String[] parseClientFileIds(String clientFileIds) {
|
||||
if (clientFileIds == null || clientFileIds.trim().isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
try {
|
||||
// Simple JSON array parsing - remove brackets and split by comma
|
||||
String trimmed = clientFileIds.trim();
|
||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||
String inside = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||
@@ -185,29 +185,39 @@ public class MergeController {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
// Adds a table of contents to the merged document using filenames as chapter titles
|
||||
private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) {
|
||||
// Create the document outline
|
||||
PDDocumentOutline outline = new PDDocumentOutline();
|
||||
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
|
||||
|
||||
int pageIndex = 0;
|
||||
int pageIndex = 0; // Current page index in the merged document
|
||||
|
||||
// Iterate through the original files
|
||||
for (MultipartFile file : files) {
|
||||
// Get the filename without extension to use as bookmark title
|
||||
String filename = file.getOriginalFilename();
|
||||
String title = GeneralUtils.removeExtension(filename);
|
||||
|
||||
// Create an outline item for this file
|
||||
PDOutlineItem item = new PDOutlineItem();
|
||||
item.setTitle(title);
|
||||
|
||||
// Set the destination to the first page of this file in the merged document
|
||||
if (pageIndex < mergedDocument.getNumberOfPages()) {
|
||||
PDPage page = mergedDocument.getPage(pageIndex);
|
||||
item.setDestination(page);
|
||||
}
|
||||
|
||||
// Add the item to the outline
|
||||
outline.addLast(item);
|
||||
|
||||
// Increment page index for the next file
|
||||
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||
pageIndex += doc.getNumberOfPages();
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("document loading for TOC generation", e);
|
||||
pageIndex++;
|
||||
pageIndex++; // Increment by at least one if we can't determine page count
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,6 +235,7 @@ public class MergeController {
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to XMP metadata if Info dates are missing
|
||||
PDMetadata metadata = doc.getDocumentCatalog().getMetadata();
|
||||
if (metadata != null) {
|
||||
try (InputStream is = metadata.createInputStream()) {
|
||||
@@ -260,10 +271,7 @@ public class MergeController {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/merge-pdfs",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/merge-pdfs")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Merge multiple PDF files into one",
|
||||
@@ -275,7 +283,7 @@ public class MergeController {
|
||||
@ModelAttribute MergePdfsRequest request,
|
||||
@RequestParam(value = "fileOrder", required = false) String fileOrder)
|
||||
throws IOException {
|
||||
List<File> filesToDelete = new ArrayList<>();
|
||||
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
||||
TempFile outputTempFile = null;
|
||||
|
||||
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
||||
@@ -286,35 +294,48 @@ public class MergeController {
|
||||
files = new MultipartFile[0];
|
||||
}
|
||||
|
||||
// If front-end provided explicit visible order, honor it and override backend sorting
|
||||
if (fileOrder != null && !fileOrder.isBlank()) {
|
||||
log.info("Reordering files based on fileOrder parameter");
|
||||
files = reorderFilesByProvidedOrder(files, fileOrder);
|
||||
} else {
|
||||
log.info("Sorting files based on sortType: {}", request.getSortType());
|
||||
Arrays.sort(files, getSortComparator(request.getSortType()));
|
||||
Arrays.sort(
|
||||
files,
|
||||
getSortComparator(
|
||||
request.getSortType())); // Sort files based on requested sort type
|
||||
}
|
||||
|
||||
try (TempFile mt = new TempFile(tempFileManager, ".pdf")) {
|
||||
|
||||
List<Path> inputPaths = new ArrayList<>(files.length);
|
||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
||||
long totalSize = 0;
|
||||
List<Integer> invalidIndexes = new ArrayList<>();
|
||||
for (int index = 0; index < files.length; index++) {
|
||||
MultipartFile multipartFile = files[index];
|
||||
File tempFile = tempFileManager.convertMultipartFileToFile(multipartFile);
|
||||
filesToDelete.add(tempFile);
|
||||
inputPaths.add(tempFile.toPath());
|
||||
totalSize += multipartFile.getSize();
|
||||
File tempFile =
|
||||
tempFileManager.convertMultipartFileToFile(
|
||||
multipartFile); // Convert MultipartFile to File
|
||||
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
||||
|
||||
try (PdfDocument ignored = PdfDocument.open(tempFile.toPath())) {
|
||||
} catch (Exception e) {
|
||||
// Pre-validate each PDF so we can report which one(s) are broken
|
||||
// Use the original MultipartFile to avoid deleting the tempFile during validation
|
||||
try (PDDocument ignored = pdfDocumentFactory.load(multipartFile)) {
|
||||
// OK
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF pre-validate", e);
|
||||
invalidIndexes.add(index);
|
||||
}
|
||||
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
||||
}
|
||||
|
||||
int[] pageCounts;
|
||||
mergerUtility.setDestinationFileName(mt.getFile().getAbsolutePath());
|
||||
|
||||
try {
|
||||
pageCounts =
|
||||
mergeWithJpdfium(inputPaths, files, generateToc, mt.getFile().toPath());
|
||||
mergerUtility.mergeDocuments(
|
||||
pdfDocumentFactory.getStreamCacheFunction(
|
||||
totalSize)); // Merge the documents
|
||||
} catch (IOException e) {
|
||||
ExceptionUtils.logException("PDF merge", e);
|
||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||
@@ -323,26 +344,10 @@ public class MergeController {
|
||||
throw e;
|
||||
}
|
||||
|
||||
boolean sigFlattenNeeded = false;
|
||||
if (removeCertSign) {
|
||||
try (PdfDocument check = PdfDocument.open(mt.getFile().toPath())) {
|
||||
sigFlattenNeeded = !check.signatures().isEmpty();
|
||||
} catch (Exception e) {
|
||||
log.debug(
|
||||
"JPDFium signature pre-check failed; falling back to PDFBox flatten:"
|
||||
+ " {}",
|
||||
e.getMessage());
|
||||
sigFlattenNeeded = true;
|
||||
}
|
||||
if (!sigFlattenNeeded) {
|
||||
log.info(
|
||||
"removeCertSign requested but merged document has no signature"
|
||||
+ " fields; skipping PDFBox flatten pass");
|
||||
}
|
||||
}
|
||||
|
||||
if (sigFlattenNeeded) {
|
||||
try (PDDocument mergedDocument = pdfDocumentFactory.load(mt.getFile())) {
|
||||
// Load the merged PDF document and operate on it inside try-with-resources
|
||||
try (PDDocument mergedDocument = pdfDocumentFactory.load(mt.getFile())) {
|
||||
// Remove signatures if removeCertSign is true
|
||||
if (removeCertSign) {
|
||||
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
||||
PDAcroForm acroForm = catalog.getAcroForm();
|
||||
if (acroForm != null) {
|
||||
@@ -350,26 +355,24 @@ public class MergeController {
|
||||
acroForm.getFields().stream()
|
||||
.filter(PDSignatureField.class::isInstance)
|
||||
.toList();
|
||||
|
||||
if (!fieldsToRemove.isEmpty()) {
|
||||
acroForm.flatten(fieldsToRemove, false);
|
||||
acroForm.flatten(
|
||||
fieldsToRemove,
|
||||
false); // Flatten the fields, effectively removing them
|
||||
}
|
||||
}
|
||||
outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||
try {
|
||||
mergedDocument.save(outputTempFile.getFile());
|
||||
} catch (Exception e) {
|
||||
outputTempFile.close();
|
||||
outputTempFile = null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
// Add table of contents if generateToc is true
|
||||
if (generateToc && files.length > 0) {
|
||||
addTableOfContents(mergedDocument, files);
|
||||
}
|
||||
|
||||
// Save the modified document to a temporary file
|
||||
outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||
try {
|
||||
Files.copy(
|
||||
mt.getFile().toPath(),
|
||||
outputTempFile.getFile().toPath(),
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
mergedDocument.save(outputTempFile.getFile());
|
||||
} catch (Exception e) {
|
||||
outputTempFile.close();
|
||||
outputTempFile = null;
|
||||
@@ -388,7 +391,7 @@ public class MergeController {
|
||||
throw ex;
|
||||
} finally {
|
||||
for (File file : filesToDelete) {
|
||||
tempFileManager.deleteTempFile(file);
|
||||
tempFileManager.deleteTempFile(file); // Delete temporary files
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,113 +401,4 @@ public class MergeController {
|
||||
|
||||
return WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName);
|
||||
}
|
||||
|
||||
private int[] mergeWithJpdfium(
|
||||
List<Path> inputPaths, MultipartFile[] files, boolean generateToc, Path outputPath)
|
||||
throws IOException {
|
||||
if (inputPaths.isEmpty()) {
|
||||
try (PdfDocument empty = PdfDocument.open(new byte[0])) {
|
||||
empty.save(outputPath);
|
||||
} catch (Exception ignored) {
|
||||
Files.write(outputPath, new byte[0]);
|
||||
}
|
||||
return new int[0];
|
||||
}
|
||||
|
||||
List<PdfDocument> docs = new ArrayList<>(inputPaths.size());
|
||||
int[] pageCounts = new int[inputPaths.size()];
|
||||
int[] pageOffsets = new int[inputPaths.size()];
|
||||
List<List<Bookmark>> sourceBookmarks = new ArrayList<>(inputPaths.size());
|
||||
int runningOffset = 0;
|
||||
try {
|
||||
for (int i = 0; i < inputPaths.size(); i++) {
|
||||
Path p = inputPaths.get(i);
|
||||
PdfDocument doc = PdfDocument.open(p);
|
||||
docs.add(doc);
|
||||
pageCounts[i] = doc.pageCount();
|
||||
pageOffsets[i] = runningOffset;
|
||||
sourceBookmarks.add(doc.bookmarks());
|
||||
runningOffset += pageCounts[i];
|
||||
}
|
||||
|
||||
BookmarkTree combinedTree =
|
||||
buildCombinedBookmarkTree(files, pageOffsets, sourceBookmarks, generateToc);
|
||||
|
||||
try (PdfDocument merged = PdfMerge.merge(docs)) {
|
||||
if (combinedTree.entries().isEmpty()) {
|
||||
merged.save(outputPath);
|
||||
} else {
|
||||
PdfBookmarkEditor.setBookmarks(merged, combinedTree, outputPath);
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw new IOException("JPDFium merge failed", e);
|
||||
} finally {
|
||||
for (PdfDocument doc : docs) {
|
||||
try {
|
||||
doc.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return pageCounts;
|
||||
}
|
||||
|
||||
private BookmarkTree buildCombinedBookmarkTree(
|
||||
MultipartFile[] files,
|
||||
int[] pageOffsets,
|
||||
List<List<Bookmark>> sourceBookmarks,
|
||||
boolean generateToc) {
|
||||
BookmarkTree.Builder builder = BookmarkTree.builder();
|
||||
|
||||
if (generateToc) {
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
String filename = files[i].getOriginalFilename();
|
||||
String title = GeneralUtils.removeExtension(filename);
|
||||
if (title == null || title.isBlank()) {
|
||||
title = "Document " + (i + 1);
|
||||
}
|
||||
builder.add(title, pageOffsets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < sourceBookmarks.size(); i++) {
|
||||
int offset = pageOffsets[i];
|
||||
for (Bookmark bm : sourceBookmarks.get(i)) {
|
||||
addBookmarkFlat(builder, bm, offset);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private void addBookmarkFlat(BookmarkTree.Builder builder, Bookmark root, int offset) {
|
||||
final int maxNodes = 100_000;
|
||||
java.util.Deque<Bookmark> stack = new java.util.ArrayDeque<>();
|
||||
java.util.Set<Bookmark> visited =
|
||||
java.util.Collections.newSetFromMap(new java.util.IdentityHashMap<>());
|
||||
stack.push(root);
|
||||
int processed = 0;
|
||||
while (!stack.isEmpty() && processed < maxNodes) {
|
||||
Bookmark bm = stack.pop();
|
||||
if (!visited.add(bm)) {
|
||||
continue;
|
||||
}
|
||||
processed++;
|
||||
if (bm.isInternal() && bm.title() != null) {
|
||||
builder.add(bm.title(), offset + bm.pageIndex());
|
||||
}
|
||||
if (bm.hasChildren()) {
|
||||
List<Bookmark> children = bm.children();
|
||||
for (int i = children.size() - 1; i >= 0; i--) {
|
||||
stack.push(children.get(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (processed >= maxNodes) {
|
||||
log.warn(
|
||||
"Source bookmark traversal hit {}-node cap; remaining bookmarks dropped",
|
||||
maxNodes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -24,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.general.MergeMultiplePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralFormCopyUtils;
|
||||
@@ -42,8 +41,7 @@ public class MultiPageLayoutController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/multi-page-layout",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Merge multiple pages of a PDF document into a single page",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.OverlayPdfsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -40,10 +39,7 @@ public class PdfOverlayController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/overlay-pdfs",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/overlay-pdfs", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Overlay PDF files in various modes",
|
||||
|
||||
+1
-5
@@ -28,7 +28,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.general.PosterPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -44,10 +43,7 @@ public class PosterPdfController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/split-for-poster-print",
|
||||
consumes = "multipart/form-data",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/split-for-poster-print", consumes = "multipart/form-data")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split large PDF pages into smaller printable chunks",
|
||||
|
||||
+2
-9
@@ -28,7 +28,6 @@ import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.SPDF.model.api.general.RearrangePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
@@ -44,10 +43,7 @@ public class RearrangePagesPDFController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/remove-pages",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-pages")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove pages from a PDF file",
|
||||
@@ -228,10 +224,7 @@ public class RearrangePagesPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/rearrange-pages",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rearrange-pages")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Rearrange pages in a PDF file",
|
||||
|
||||
+1
-5
@@ -19,7 +19,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.general.RotatePDFRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -33,10 +32,7 @@ public class RotationController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/rotate-pdf",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/rotate-pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Rotate a PDF file",
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.general.ScalePagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -115,10 +114,7 @@ public class ScalePagesController {
|
||||
return sizeMap;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/scale-pages",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/scale-pages", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Change the size of a PDF page/document",
|
||||
description =
|
||||
|
||||
+1
-4
@@ -16,7 +16,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SettingsApi;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
|
||||
@@ -28,9 +27,7 @@ public class SettingsController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/update-enable-analytics",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping("/update-enable-analytics")
|
||||
@Hidden
|
||||
public ResponseEntity<Map<String, Object>> updateApiKey(@RequestParam Boolean enabled)
|
||||
throws IOException {
|
||||
|
||||
+53
-64
@@ -3,10 +3,11 @@ package stirling.software.SPDF.controller.api;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
@@ -27,15 +28,13 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.SplitPagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfSplit;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@@ -45,10 +44,7 @@ public class SplitPDFController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/split-pages",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/split-pages")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split a PDF file into separate documents",
|
||||
@@ -90,8 +86,13 @@ public class SplitPDFController {
|
||||
String baseFilename = GeneralUtils.removeExtension(file.getOriginalFilename());
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(Files.newOutputStream(outputTempFile.getPath()))) {
|
||||
writeSplits(
|
||||
sourceTempFile.getFile(), pageNumbers, baseFilename, zipOut, hasForm);
|
||||
if (hasForm) {
|
||||
writeSplitsViaReload(
|
||||
sourceTempFile.getFile(), pageNumbers, baseFilename, zipOut);
|
||||
} else {
|
||||
writeSplitsViaSharedSource(
|
||||
sourceTempFile.getFile(), pageNumbers, baseFilename, zipOut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,71 +105,59 @@ public class SplitPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSplits(
|
||||
File source,
|
||||
List<Integer> pageNumbers,
|
||||
String baseFilename,
|
||||
ZipOutputStream zipOut,
|
||||
boolean hasForm)
|
||||
private void writeSplitsViaReload(
|
||||
File source, List<Integer> pageNumbers, String baseFilename, ZipOutputStream zipOut)
|
||||
throws IOException {
|
||||
try (PdfDocument sourceDoc = PdfDocument.open(source.toPath())) {
|
||||
int previousPageNumber = 0;
|
||||
for (int splitIndex = 0; splitIndex < pageNumbers.size(); splitIndex++) {
|
||||
int splitPoint = pageNumbers.get(splitIndex);
|
||||
Set<Integer> keep = new HashSet<>();
|
||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||
keep.add(i);
|
||||
}
|
||||
previousPageNumber = splitPoint + 1;
|
||||
|
||||
try (PDDocument splitDoc = pdfDocumentFactory.load(source)) {
|
||||
for (int p = splitDoc.getNumberOfPages() - 1; p >= 0; p--) {
|
||||
if (!keep.contains(p)) {
|
||||
splitDoc.removePage(p);
|
||||
}
|
||||
}
|
||||
FormUtils.pruneOrphanedFormFields(splitDoc);
|
||||
writeEntry(zipOut, baseFilename, splitIndex + 1, splitDoc);
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("document splitting and saving", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSplitsViaSharedSource(
|
||||
File source, List<Integer> pageNumbers, String baseFilename, ZipOutputStream zipOut)
|
||||
throws IOException {
|
||||
try (PDDocument sourceDoc = pdfDocumentFactory.load(source)) {
|
||||
int previousPageNumber = 0;
|
||||
for (int splitIndex = 0; splitIndex < pageNumbers.size(); splitIndex++) {
|
||||
int splitPoint = pageNumbers.get(splitIndex);
|
||||
writeSplit(
|
||||
sourceDoc,
|
||||
previousPageNumber,
|
||||
splitPoint,
|
||||
baseFilename,
|
||||
splitIndex + 1,
|
||||
zipOut,
|
||||
hasForm);
|
||||
previousPageNumber = splitPoint + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSplit(
|
||||
PdfDocument sourceDoc,
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
String baseFilename,
|
||||
int splitNumber,
|
||||
ZipOutputStream zipOut,
|
||||
boolean hasForm)
|
||||
throws IOException {
|
||||
try (TempFile splitTemp = new TempFile(tempFileManager, ".pdf")) {
|
||||
try (PdfDocument splitDoc = PdfSplit.extractPageRange(sourceDoc, fromIndex, toIndex)) {
|
||||
splitDoc.save(splitTemp.getPath());
|
||||
}
|
||||
Path finalPath = splitTemp.getPath();
|
||||
TempFile prunedTemp = null;
|
||||
try {
|
||||
if (hasForm) {
|
||||
prunedTemp = new TempFile(tempFileManager, ".pdf");
|
||||
pruneForms(splitTemp.getFile(), prunedTemp.getFile());
|
||||
finalPath = prunedTemp.getPath();
|
||||
}
|
||||
writeEntry(zipOut, baseFilename, splitNumber, finalPath);
|
||||
} finally {
|
||||
if (prunedTemp != null) {
|
||||
prunedTemp.close();
|
||||
try (PDDocument splitDoc =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDoc)) {
|
||||
for (int i = previousPageNumber; i <= splitPoint; i++) {
|
||||
splitDoc.addPage(sourceDoc.getPage(i));
|
||||
}
|
||||
previousPageNumber = splitPoint + 1;
|
||||
writeEntry(zipOut, baseFilename, splitIndex + 1, splitDoc);
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("document splitting and saving", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneForms(File splitFile, File outputFile) throws IOException {
|
||||
try (PDDocument doc = pdfDocumentFactory.load(splitFile)) {
|
||||
FormUtils.pruneOrphanedFormFields(doc);
|
||||
doc.save(outputFile);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEntry(ZipOutputStream zipOut, String baseFilename, int index, Path pdfPath)
|
||||
private void writeEntry(ZipOutputStream zipOut, String baseFilename, int index, PDDocument doc)
|
||||
throws IOException {
|
||||
zipOut.putNextEntry(new ZipEntry(baseFilename + "_" + index + ".pdf"));
|
||||
Files.copy(pdfPath, zipOut);
|
||||
doc.save(zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
+132
-125
@@ -1,9 +1,6 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -11,6 +8,9 @@ import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDDocumentOutline;
|
||||
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -30,7 +30,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.PdfMetadata;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
@@ -39,8 +38,6 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfSplit;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@@ -53,42 +50,77 @@ public class SplitPdfByChaptersController {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
private static void collectBookmarks(
|
||||
List<stirling.software.jpdfium.doc.Bookmark> source,
|
||||
List<Bookmark> out,
|
||||
private static List<Bookmark> extractOutlineItems(
|
||||
PDDocument sourceDocument,
|
||||
PDOutlineItem current,
|
||||
List<Bookmark> bookmarks,
|
||||
PDOutlineItem nextParent,
|
||||
int level,
|
||||
int maxLevel) {
|
||||
for (stirling.software.jpdfium.doc.Bookmark bm : source) {
|
||||
if (!bm.isInternal()) {
|
||||
continue;
|
||||
}
|
||||
String title = bm.title() == null ? "" : bm.title().replace("/", "");
|
||||
int firstPage = Math.max(0, bm.pageIndex());
|
||||
out.add(new Bookmark(title, firstPage, -2));
|
||||
if (bm.hasChildren() && level < maxLevel) {
|
||||
collectBookmarks(bm.children(), out, level + 1, maxLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
int maxLevel)
|
||||
throws Exception {
|
||||
|
||||
private static void assignEndPages(List<Bookmark> bookmarks, int totalPages) {
|
||||
for (int i = 0; i < bookmarks.size(); i++) {
|
||||
Bookmark current = bookmarks.get(i);
|
||||
int next = -1;
|
||||
for (int j = i + 1; j < bookmarks.size(); j++) {
|
||||
if (bookmarks.get(j).getStartPage() >= current.getStartPage()) {
|
||||
next = bookmarks.get(j).getStartPage();
|
||||
break;
|
||||
}
|
||||
while (current != null) {
|
||||
|
||||
String currentTitle = current.getTitle().replace("/", "");
|
||||
int firstPage =
|
||||
sourceDocument.getPages().indexOf(current.findDestinationPage(sourceDocument));
|
||||
PDOutlineItem child = current.getFirstChild();
|
||||
PDOutlineItem nextSibling = current.getNextSibling();
|
||||
int endPage;
|
||||
if (child != null && level < maxLevel) {
|
||||
endPage =
|
||||
sourceDocument
|
||||
.getPages()
|
||||
.indexOf(child.findDestinationPage(sourceDocument));
|
||||
} else if (nextSibling != null) {
|
||||
endPage =
|
||||
sourceDocument
|
||||
.getPages()
|
||||
.indexOf(nextSibling.findDestinationPage(sourceDocument));
|
||||
} else if (nextParent != null) {
|
||||
|
||||
endPage =
|
||||
sourceDocument
|
||||
.getPages()
|
||||
.indexOf(nextParent.findDestinationPage(sourceDocument));
|
||||
} else {
|
||||
endPage = -2;
|
||||
/*
|
||||
happens when we have something like this:
|
||||
Outline Item 2
|
||||
Outline Item 2.1
|
||||
Outline Item 2.1.1
|
||||
Outline Item 2.2
|
||||
Outline 2.2.1
|
||||
Outline 2.2.2 <--- this item neither has an immediate next parent nor an immediate next sibling
|
||||
Outline Item 3
|
||||
*/
|
||||
}
|
||||
current.setEndPage(next == -1 ? totalPages : next);
|
||||
if (!bookmarks.isEmpty()
|
||||
&& bookmarks.get(bookmarks.size() - 1).getEndPage() == -2
|
||||
&& firstPage
|
||||
>= bookmarks
|
||||
.get(bookmarks.size() - 1)
|
||||
.getStartPage()) { // for handling the above-mentioned case
|
||||
Bookmark previousBookmark = bookmarks.get(bookmarks.size() - 1);
|
||||
previousBookmark.setEndPage(firstPage);
|
||||
}
|
||||
bookmarks.add(new Bookmark(currentTitle, firstPage, endPage));
|
||||
|
||||
// Recursively process children
|
||||
if (child != null && level < maxLevel) {
|
||||
extractOutlineItems(
|
||||
sourceDocument, child, bookmarks, nextSibling, level + 1, maxLevel);
|
||||
}
|
||||
|
||||
current = nextSibling;
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/split-pdf-by-chapters",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split PDFs by Chapters",
|
||||
@@ -100,41 +132,46 @@ public class SplitPdfByChaptersController {
|
||||
MultipartFile file = request.getFileInput();
|
||||
|
||||
boolean includeMetadata = Boolean.TRUE.equals(request.getIncludeMetadata());
|
||||
Integer bookmarkLevel = request.getBookmarkLevel();
|
||||
Integer bookmarkLevel =
|
||||
request.getBookmarkLevel(); // levels start from 0 (top most bookmarks)
|
||||
if (bookmarkLevel < 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument", "Invalid argument: {0}", "bookmark level");
|
||||
}
|
||||
|
||||
try (TempFile sourceTempFile = new TempFile(tempFileManager, ".pdf")) {
|
||||
Files.copy(
|
||||
file.getInputStream(),
|
||||
sourceTempFile.getPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
try (PDDocument sourceDocument = pdfDocumentFactory.load(file)) {
|
||||
PDDocumentOutline outline = sourceDocument.getDocumentCatalog().getDocumentOutline();
|
||||
|
||||
if (outline == null) {
|
||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pdfBookmarksNotFound", "No PDF bookmarks/outline found in document");
|
||||
}
|
||||
List<Bookmark> bookmarks = new ArrayList<>();
|
||||
int totalPages;
|
||||
try (PdfDocument sourceDocument = PdfDocument.open(sourceTempFile.getPath())) {
|
||||
totalPages = sourceDocument.pageCount();
|
||||
List<stirling.software.jpdfium.doc.Bookmark> roots = sourceDocument.bookmarks();
|
||||
if (roots == null || roots.isEmpty()) {
|
||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pdfBookmarksNotFound",
|
||||
"No PDF bookmarks/outline found in document");
|
||||
}
|
||||
collectBookmarks(roots, bookmarks, 0, bookmarkLevel);
|
||||
if (bookmarks.isEmpty()) {
|
||||
log.warn("No outline found for {}", file.getOriginalFilename());
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.pdfBookmarksNotFound",
|
||||
"No PDF bookmarks/outline found in document");
|
||||
}
|
||||
assignEndPages(bookmarks, totalPages);
|
||||
try {
|
||||
bookmarks =
|
||||
extractOutlineItems(
|
||||
sourceDocument,
|
||||
outline.getFirstChild(),
|
||||
bookmarks,
|
||||
outline.getFirstChild().getNextSibling(),
|
||||
0,
|
||||
bookmarkLevel);
|
||||
// to handle last page edge case
|
||||
bookmarks.get(bookmarks.size() - 1).setEndPage(sourceDocument.getNumberOfPages());
|
||||
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("outline extraction", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
boolean allowDuplicates = Boolean.TRUE.equals(request.getAllowDuplicates());
|
||||
if (!allowDuplicates) {
|
||||
/*
|
||||
duplicates are generated when multiple bookmarks correspond to the same page,
|
||||
if the user doesn't want duplicates mergeBookmarksThatCorrespondToSamePage() method will merge the titles of all
|
||||
the bookmarks that correspond to the same page, and treat them as a single bookmark
|
||||
*/
|
||||
bookmarks = mergeBookmarksThatCorrespondToSamePage(bookmarks);
|
||||
}
|
||||
for (Bookmark bookmark : bookmarks) {
|
||||
@@ -145,15 +182,7 @@ public class SplitPdfByChaptersController {
|
||||
bookmark.getEndPage());
|
||||
}
|
||||
|
||||
PdfMetadata metadata = null;
|
||||
if (includeMetadata) {
|
||||
try (PDDocument metaDoc = pdfDocumentFactory.load(sourceTempFile.getFile())) {
|
||||
metadata = pdfMetadataService.extractMetadataFromPdf(metaDoc);
|
||||
}
|
||||
}
|
||||
|
||||
TempFile zipTempFile =
|
||||
createZipFile(sourceTempFile.getFile(), bookmarks, metadata, totalPages);
|
||||
TempFile zipTempFile = createZipFile(sourceDocument, bookmarks, includeMetadata);
|
||||
String filename = GeneralUtils.generateFilename(file.getOriginalFilename(), "");
|
||||
return WebResponseUtils.zipFileToWebResponse(zipTempFile, filename + ".zip");
|
||||
}
|
||||
@@ -185,23 +214,45 @@ public class SplitPdfByChaptersController {
|
||||
}
|
||||
|
||||
private TempFile createZipFile(
|
||||
File sourceFile, List<Bookmark> bookmarks, PdfMetadata metadata, int totalPages)
|
||||
PDDocument sourceDocument, List<Bookmark> bookmarks, boolean includeMetadata)
|
||||
throws Exception {
|
||||
PdfMetadata metadata =
|
||||
includeMetadata ? pdfMetadataService.extractMetadataFromPdf(sourceDocument) : null;
|
||||
String fileNumberFormatter = "%0" + (Integer.toString(bookmarks.size()).length()) + "d ";
|
||||
TempFile zipTempFile = new TempFile(tempFileManager, ".zip");
|
||||
try (PdfDocument sourceDocument = PdfDocument.open(sourceFile.toPath());
|
||||
ZipOutputStream zipOut =
|
||||
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
|
||||
for (int i = 0; i < bookmarks.size(); i++) {
|
||||
Bookmark bookmark = bookmarks.get(i);
|
||||
writeChapter(
|
||||
sourceDocument,
|
||||
bookmark,
|
||||
i,
|
||||
fileNumberFormatter,
|
||||
metadata,
|
||||
zipOut,
|
||||
totalPages);
|
||||
try {
|
||||
try (ZipOutputStream zipOut =
|
||||
new ZipOutputStream(Files.newOutputStream(zipTempFile.getPath()))) {
|
||||
for (int i = 0; i < bookmarks.size(); i++) {
|
||||
Bookmark bookmark = bookmarks.get(i);
|
||||
try (PDDocument splitDocument = new PDDocument()) {
|
||||
boolean isSinglePage = (bookmark.getStartPage() == bookmark.getEndPage());
|
||||
|
||||
for (int pg = bookmark.getStartPage();
|
||||
pg < bookmark.getEndPage() + (isSinglePage ? 1 : 0);
|
||||
pg++) {
|
||||
PDPage page = sourceDocument.getPage(pg);
|
||||
splitDocument.addPage(page);
|
||||
log.debug("Adding page {} to split document", pg);
|
||||
}
|
||||
if (includeMetadata) {
|
||||
pdfMetadataService.setMetadataToPdf(splitDocument, metadata);
|
||||
}
|
||||
|
||||
// split files will be named as "[FILE_NUMBER] [BOOKMARK_TITLE].pdf"
|
||||
String fileName =
|
||||
String.format(Locale.ROOT, fileNumberFormatter, i)
|
||||
+ bookmark.getTitle()
|
||||
+ ".pdf";
|
||||
zipOut.putNextEntry(new ZipEntry(fileName));
|
||||
splitDocument.save(zipOut);
|
||||
zipOut.closeEntry();
|
||||
log.debug("Wrote split document {} to zip file", fileName);
|
||||
} catch (Exception e) {
|
||||
ExceptionUtils.logException("document splitting and saving", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info(
|
||||
"Successfully created zip file with split documents: {}",
|
||||
@@ -212,50 +263,6 @@ public class SplitPdfByChaptersController {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeChapter(
|
||||
PdfDocument sourceDocument,
|
||||
Bookmark bookmark,
|
||||
int index,
|
||||
String fileNumberFormatter,
|
||||
PdfMetadata metadata,
|
||||
ZipOutputStream zipOut,
|
||||
int totalPages)
|
||||
throws Exception {
|
||||
boolean isSinglePage = (bookmark.getStartPage() == bookmark.getEndPage());
|
||||
int from = Math.min(Math.max(0, bookmark.getStartPage()), totalPages - 1);
|
||||
int rawEnd = isSinglePage ? bookmark.getEndPage() : bookmark.getEndPage() - 1;
|
||||
int to = Math.min(Math.max(from, rawEnd), totalPages - 1);
|
||||
try (TempFile splitTemp = new TempFile(tempFileManager, ".pdf")) {
|
||||
try (PdfDocument splitDoc = PdfSplit.extractPageRange(sourceDocument, from, to)) {
|
||||
splitDoc.save(splitTemp.getPath());
|
||||
}
|
||||
Path finalPath = splitTemp.getPath();
|
||||
TempFile metaTemp = null;
|
||||
try {
|
||||
if (metadata != null) {
|
||||
metaTemp = new TempFile(tempFileManager, ".pdf");
|
||||
try (PDDocument doc = pdfDocumentFactory.load(splitTemp.getFile())) {
|
||||
pdfMetadataService.setMetadataToPdf(doc, metadata);
|
||||
doc.save(metaTemp.getFile());
|
||||
}
|
||||
finalPath = metaTemp.getPath();
|
||||
}
|
||||
String fileName =
|
||||
String.format(Locale.ROOT, fileNumberFormatter, index)
|
||||
+ bookmark.getTitle()
|
||||
+ ".pdf";
|
||||
zipOut.putNextEntry(new ZipEntry(fileName));
|
||||
Files.copy(finalPath, zipOut);
|
||||
zipOut.closeEntry();
|
||||
log.debug("Wrote split document {} to zip file", fileName);
|
||||
} finally {
|
||||
if (metaTemp != null) {
|
||||
metaTemp.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
|
||||
+1
-3
@@ -33,7 +33,6 @@ import stirling.software.SPDF.model.SplitTypes;
|
||||
import stirling.software.SPDF.model.api.SplitPdfBySectionsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -51,8 +50,7 @@ public class SplitPdfBySectionsController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/split-pdf-by-sections",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
value = "/split-pdf-by-sections")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Split PDF pages into smaller sections",
|
||||
|
||||
+135
-126
@@ -1,16 +1,19 @@
|
||||
package stirling.software.SPDF.controller.api;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -26,7 +29,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.general.SplitPdfBySizeOrCountRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.FormUtils;
|
||||
@@ -34,8 +36,6 @@ import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfSplit;
|
||||
|
||||
@GeneralApi
|
||||
@Slf4j
|
||||
@@ -47,8 +47,7 @@ public class SplitPdfBySizeController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/split-by-size-or-count",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents based on size or count",
|
||||
@@ -74,20 +73,23 @@ public class SplitPdfBySizeController {
|
||||
sourceTempFile.getPath(),
|
||||
StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
boolean hasForm;
|
||||
try (PDDocument acroDoc = pdfDocumentFactory.load(sourceTempFile.getFile(), true)) {
|
||||
hasForm = acroDoc.getDocumentCatalog().getAcroForm(null) != null;
|
||||
}
|
||||
|
||||
try (PdfDocument sourceDocument = PdfDocument.open(sourceTempFile.getPath())) {
|
||||
List<int[]> ranges = computeRanges(request, sourceDocument);
|
||||
try (PDDocument sourceDocument =
|
||||
pdfDocumentFactory.load(sourceTempFile.getFile(), true)) {
|
||||
boolean hasForm = sourceDocument.getDocumentCatalog().getAcroForm(null) != null;
|
||||
List<List<Integer>> ranges = computeRanges(request, sourceDocument);
|
||||
|
||||
int fileIndex = 1;
|
||||
for (int[] range : ranges) {
|
||||
if (range.length == 0) {
|
||||
for (List<Integer> range : ranges) {
|
||||
if (range.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
writeRange(sourceDocument, range, zipOut, filename, fileIndex++, hasForm);
|
||||
if (hasForm) {
|
||||
writeRangeViaReload(
|
||||
sourceTempFile.getFile(), range, zipOut, filename, fileIndex++);
|
||||
} else {
|
||||
writeRangeViaSharedSource(
|
||||
sourceDocument, range, zipOut, filename, fileIndex++);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,80 +102,80 @@ public class SplitPdfBySizeController {
|
||||
}
|
||||
}
|
||||
|
||||
private List<int[]> computeRanges(SplitPdfBySizeOrCountRequest request, PdfDocument sourceDoc)
|
||||
throws IOException {
|
||||
private List<List<Integer>> computeRanges(
|
||||
SplitPdfBySizeOrCountRequest request, PDDocument sourceDocument) throws IOException {
|
||||
int type = request.getSplitType();
|
||||
String value = request.getSplitValue();
|
||||
if (type == 0) {
|
||||
return computeSizeRanges(sourceDoc, GeneralUtils.convertSizeToBytes(value));
|
||||
return computeSizeRanges(sourceDocument, GeneralUtils.convertSizeToBytes(value));
|
||||
} else if (type == 1) {
|
||||
return computePageCountRanges(sourceDoc, Integer.parseInt(value));
|
||||
return computePageCountRanges(sourceDocument, Integer.parseInt(value));
|
||||
} else if (type == 2) {
|
||||
return computeDocCountRanges(sourceDoc, Integer.parseInt(value));
|
||||
return computeDocCountRanges(sourceDocument, Integer.parseInt(value));
|
||||
}
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument", "Invalid argument: {0}", "split type: " + type);
|
||||
}
|
||||
|
||||
private void writeRange(
|
||||
PdfDocument sourceDoc,
|
||||
int[] range,
|
||||
private void writeRangeViaReload(
|
||||
File sourceFile,
|
||||
List<Integer> keepIndices,
|
||||
ZipOutputStream zipOut,
|
||||
String baseFilename,
|
||||
int fileIndex,
|
||||
boolean hasForm)
|
||||
int fileIndex)
|
||||
throws IOException {
|
||||
try (TempFile splitTemp = new TempFile(tempFileManager, ".pdf")) {
|
||||
extractRangeToFile(sourceDoc, range, splitTemp.getPath());
|
||||
Path finalPath = splitTemp.getPath();
|
||||
TempFile prunedTemp = null;
|
||||
try {
|
||||
if (hasForm) {
|
||||
prunedTemp = new TempFile(tempFileManager, ".pdf");
|
||||
try (PDDocument doc = pdfDocumentFactory.load(splitTemp.getFile())) {
|
||||
FormUtils.pruneOrphanedFormFields(doc);
|
||||
doc.save(prunedTemp.getFile());
|
||||
}
|
||||
finalPath = prunedTemp.getPath();
|
||||
}
|
||||
writeEntry(zipOut, baseFilename, fileIndex, finalPath);
|
||||
} finally {
|
||||
if (prunedTemp != null) {
|
||||
prunedTemp.close();
|
||||
Set<Integer> keep = new HashSet<>(keepIndices);
|
||||
try (PDDocument doc = pdfDocumentFactory.load(sourceFile)) {
|
||||
for (int i = doc.getNumberOfPages() - 1; i >= 0; i--) {
|
||||
if (!keep.contains(i)) {
|
||||
doc.removePage(i);
|
||||
}
|
||||
}
|
||||
FormUtils.pruneOrphanedFormFields(doc);
|
||||
writeEntry(zipOut, baseFilename, fileIndex, doc);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractRangeToFile(PdfDocument sourceDoc, int[] range, Path outputPath)
|
||||
private void writeRangeViaSharedSource(
|
||||
PDDocument sourceDocument,
|
||||
List<Integer> keepIndices,
|
||||
ZipOutputStream zipOut,
|
||||
String baseFilename,
|
||||
int fileIndex)
|
||||
throws IOException {
|
||||
int from = range[0];
|
||||
int to = range[range.length - 1];
|
||||
try (PdfDocument split = PdfSplit.extractPageRange(sourceDoc, from, to)) {
|
||||
split.save(outputPath);
|
||||
try (PDDocument doc =
|
||||
pdfDocumentFactory.createNewDocumentBasedOnOldDocument(sourceDocument)) {
|
||||
for (int p : keepIndices) {
|
||||
doc.addPage(sourceDocument.getPage(p));
|
||||
}
|
||||
writeEntry(zipOut, baseFilename, fileIndex, doc);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeEntry(
|
||||
ZipOutputStream zipOut, String baseFilename, int fileIndex, Path pdfPath)
|
||||
ZipOutputStream zipOut, String baseFilename, int fileIndex, PDDocument doc)
|
||||
throws IOException {
|
||||
zipOut.putNextEntry(new ZipEntry(baseFilename + "_" + fileIndex + ".pdf"));
|
||||
Files.copy(pdfPath, zipOut);
|
||||
doc.save(zipOut);
|
||||
zipOut.closeEntry();
|
||||
}
|
||||
|
||||
/** Returns contiguous page-index ranges fitting within {@code maxBytes}. */
|
||||
private List<int[]> computeSizeRanges(PdfDocument sourceDoc, long maxBytes) throws IOException {
|
||||
List<int[]> ranges = new ArrayList<>();
|
||||
int totalPages = sourceDoc.pageCount();
|
||||
/** Page-index ranges each output should contain. AcroForm overhead isn't modeled. */
|
||||
private List<List<Integer>> computeSizeRanges(PDDocument sourceDocument, long maxBytes)
|
||||
throws IOException {
|
||||
List<List<Integer>> ranges = new ArrayList<>();
|
||||
List<Integer> currentRange = new ArrayList<>();
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
int baseCheckFrequency = 5;
|
||||
int rangeStart = 0;
|
||||
int rangeEnd = -1;
|
||||
try (TempFile probe = new TempFile(tempFileManager, ".pdf")) {
|
||||
File probeFile = probe.getFile();
|
||||
|
||||
PDDocument scratch = new PDDocument();
|
||||
try {
|
||||
for (int pageIndex = 0; pageIndex < totalPages; pageIndex++) {
|
||||
rangeEnd = pageIndex;
|
||||
int pageAdded = rangeEnd - rangeStart + 1;
|
||||
PDPage page = sourceDocument.getPage(pageIndex);
|
||||
scratch.addPage(new PDPage(page.getCOSObject()));
|
||||
currentRange.add(pageIndex);
|
||||
|
||||
int pageAdded = currentRange.size();
|
||||
boolean shouldCheckSize =
|
||||
(pageAdded % baseCheckFrequency == 0)
|
||||
|| (pageIndex == totalPages - 1)
|
||||
@@ -181,110 +183,117 @@ public class SplitPdfBySizeController {
|
||||
if (!shouldCheckSize) {
|
||||
continue;
|
||||
}
|
||||
long actualSize = saveRange(sourceDoc, rangeStart, rangeEnd, probeFile);
|
||||
|
||||
long actualSize;
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
scratch.save(out);
|
||||
actualSize = out.size();
|
||||
}
|
||||
|
||||
if (actualSize > maxBytes) {
|
||||
if (pageAdded > 1) {
|
||||
rangeEnd = pageIndex - 1;
|
||||
pageIndex--;
|
||||
if (scratch.getNumberOfPages() > 1) {
|
||||
scratch.removePage(scratch.getNumberOfPages() - 1);
|
||||
currentRange.remove(currentRange.size() - 1);
|
||||
pageIndex--; // retry this page in the next chunk
|
||||
}
|
||||
ranges.add(buildRange(rangeStart, rangeEnd));
|
||||
rangeStart = rangeEnd + 1;
|
||||
rangeEnd = rangeStart - 1;
|
||||
ranges.add(new ArrayList<>(currentRange));
|
||||
currentRange.clear();
|
||||
scratch.close();
|
||||
scratch = new PDDocument();
|
||||
} else if (pageIndex < totalPages - 1 && actualSize < maxBytes * 0.75) {
|
||||
int extra =
|
||||
lookAheadFit(
|
||||
sourceDoc,
|
||||
rangeStart,
|
||||
pageIndex,
|
||||
maxBytes,
|
||||
totalPages,
|
||||
probeFile);
|
||||
pageIndex += extra;
|
||||
rangeEnd = pageIndex;
|
||||
int extraPagesAdded =
|
||||
lookAheadFit(scratch, sourceDocument, pageIndex, maxBytes);
|
||||
for (int i = 0; i < extraPagesAdded; i++) {
|
||||
int extra = pageIndex + 1 + i;
|
||||
scratch.addPage(new PDPage(sourceDocument.getPage(extra).getCOSObject()));
|
||||
currentRange.add(extra);
|
||||
}
|
||||
pageIndex += extraPagesAdded;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (rangeEnd >= rangeStart) {
|
||||
ranges.add(buildRange(rangeStart, rangeEnd));
|
||||
|
||||
if (!currentRange.isEmpty()) {
|
||||
ranges.add(new ArrayList<>(currentRange));
|
||||
}
|
||||
} finally {
|
||||
scratch.close();
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private long saveRange(PdfDocument sourceDoc, int from, int to, File output)
|
||||
/** Speculatively tries up to 5 next pages; returns how many fit under {@code maxBytes}. */
|
||||
private int lookAheadFit(PDDocument scratch, PDDocument source, int pageIndex, long maxBytes)
|
||||
throws IOException {
|
||||
try (PdfDocument split = PdfSplit.extractPageRange(sourceDoc, from, to)) {
|
||||
split.save(output.toPath());
|
||||
int totalPages = source.getNumberOfPages();
|
||||
int pagesToLookAhead = Math.min(5, totalPages - pageIndex - 1);
|
||||
if (pagesToLookAhead == 0) {
|
||||
return 0;
|
||||
}
|
||||
return output.length();
|
||||
}
|
||||
|
||||
private int lookAheadFit(
|
||||
PdfDocument sourceDoc,
|
||||
int rangeStart,
|
||||
int currentEnd,
|
||||
long maxBytes,
|
||||
int totalPages,
|
||||
File probeFile)
|
||||
throws IOException {
|
||||
int pagesToLookAhead = Math.min(5, totalPages - currentEnd - 1);
|
||||
int extra = 0;
|
||||
for (int i = 0; i < pagesToLookAhead; i++) {
|
||||
int trialEnd = currentEnd + 1 + i;
|
||||
long size = saveRange(sourceDoc, rangeStart, trialEnd, probeFile);
|
||||
if (size > maxBytes) {
|
||||
break;
|
||||
int extraPagesAdded = 0;
|
||||
try (PDDocument testDoc = new PDDocument()) {
|
||||
for (int i = 0; i < scratch.getNumberOfPages(); i++) {
|
||||
testDoc.addPage(new PDPage(scratch.getPage(i).getCOSObject()));
|
||||
}
|
||||
for (int i = 0; i < pagesToLookAhead; i++) {
|
||||
testDoc.addPage(new PDPage(source.getPage(pageIndex + 1 + i).getCOSObject()));
|
||||
long testSize;
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
testDoc.save(out);
|
||||
testSize = out.size();
|
||||
}
|
||||
if (testSize > maxBytes) {
|
||||
break;
|
||||
}
|
||||
extraPagesAdded++;
|
||||
}
|
||||
extra++;
|
||||
}
|
||||
return extra;
|
||||
return extraPagesAdded;
|
||||
}
|
||||
|
||||
private List<int[]> computePageCountRanges(PdfDocument sourceDoc, int pageCount) {
|
||||
private List<List<Integer>> computePageCountRanges(PDDocument sourceDocument, int pageCount) {
|
||||
if (pageCount <= 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument", "Invalid argument: {0}", "page count: " + pageCount);
|
||||
}
|
||||
int totalPages = sourceDoc.pageCount();
|
||||
List<int[]> ranges = new ArrayList<>();
|
||||
int start = 0;
|
||||
while (start < totalPages) {
|
||||
int end = Math.min(start + pageCount - 1, totalPages - 1);
|
||||
ranges.add(buildRange(start, end));
|
||||
start = end + 1;
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
List<List<Integer>> ranges = new ArrayList<>();
|
||||
List<Integer> current = new ArrayList<>(pageCount);
|
||||
for (int i = 0; i < totalPages; i++) {
|
||||
current.add(i);
|
||||
if (current.size() == pageCount) {
|
||||
ranges.add(current);
|
||||
current = new ArrayList<>(pageCount);
|
||||
}
|
||||
}
|
||||
if (!current.isEmpty()) {
|
||||
ranges.add(current);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private List<int[]> computeDocCountRanges(PdfDocument sourceDoc, int documentCount) {
|
||||
private List<List<Integer>> computeDocCountRanges(
|
||||
PDDocument sourceDocument, int documentCount) {
|
||||
if (documentCount <= 0) {
|
||||
throw ExceptionUtils.createIllegalArgumentException(
|
||||
"error.invalidArgument",
|
||||
"Invalid argument: {0}",
|
||||
"document count: " + documentCount);
|
||||
}
|
||||
int totalPages = sourceDoc.pageCount();
|
||||
int totalPages = sourceDocument.getNumberOfPages();
|
||||
int pagesPerDocument = totalPages / documentCount;
|
||||
int extraPages = totalPages % documentCount;
|
||||
List<int[]> ranges = new ArrayList<>();
|
||||
|
||||
List<List<Integer>> ranges = new ArrayList<>();
|
||||
int cursor = 0;
|
||||
for (int i = 0; i < documentCount; i++) {
|
||||
int pagesToAdd = pagesPerDocument + (i < extraPages ? 1 : 0);
|
||||
if (pagesToAdd == 0) {
|
||||
continue;
|
||||
List<Integer> range = new ArrayList<>(pagesToAdd);
|
||||
for (int j = 0; j < pagesToAdd; j++) {
|
||||
range.add(cursor++);
|
||||
}
|
||||
int end = cursor + pagesToAdd - 1;
|
||||
ranges.add(buildRange(cursor, end));
|
||||
cursor = end + 1;
|
||||
ranges.add(range);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static int[] buildRange(int start, int end) {
|
||||
int[] range = new int[end - start + 1];
|
||||
for (int i = 0; i < range.length; i++) {
|
||||
range[i] = start + i;
|
||||
}
|
||||
return range;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -36,8 +35,7 @@ public class ToSinglePageController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf-to-single-page",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
value = "/pdf-to-single-page")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a multi-page PDF into a single long page PDF",
|
||||
|
||||
+1
-5
@@ -28,7 +28,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertEbookToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -57,10 +56,7 @@ public class ConvertEbookToPDFController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/ebook/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ebook/pdf")
|
||||
@Operation(
|
||||
summary = "Convert an eBook file to PDF",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||
@@ -44,10 +43,7 @@ public class ConvertEmlToPDF {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/eml/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/eml/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert EML/MSG to PDF",
|
||||
|
||||
+1
-5
@@ -17,7 +17,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.*;
|
||||
@@ -34,10 +33,7 @@ public class ConvertHtmlToPDF {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/html/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/html/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert an HTML or ZIP (containing HTML and CSS) to PDF",
|
||||
|
||||
+2
-9
@@ -40,7 +40,6 @@ import stirling.software.SPDF.model.api.converters.ConvertToImageRequest;
|
||||
import stirling.software.SPDF.model.api.converters.ConvertToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CbrUtils;
|
||||
import stirling.software.common.util.CbzUtils;
|
||||
@@ -73,10 +72,7 @@ public class ConvertImgPDFController {
|
||||
return endpointConfiguration.isGroupEnabled("Ghostscript");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/img",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/img")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Convert PDF to image(s)",
|
||||
@@ -243,10 +239,7 @@ public class ConvertImgPDFController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/img/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/img/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert images to a PDF file",
|
||||
|
||||
+1
-5
@@ -26,7 +26,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.*;
|
||||
@@ -42,10 +41,7 @@ public class ConvertMarkdownToPdf {
|
||||
|
||||
private final CustomHtmlSanitizer customHtmlSanitizer;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/markdown/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/markdown/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert a Markdown file to PDF",
|
||||
|
||||
+1
-5
@@ -29,7 +29,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.CustomHtmlSanitizer;
|
||||
@@ -201,10 +200,7 @@ public class ConvertOfficeController {
|
||||
.matches();
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/file/pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/file/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a file to a PDF using LibreOffice",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -27,7 +27,6 @@ import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.Outpu
|
||||
import stirling.software.SPDF.model.api.converters.ConvertPdfToEpubRequest.TargetDevice;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
@@ -82,10 +81,7 @@ public class ConvertPDFToEpubController {
|
||||
return command;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/epub",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/epub")
|
||||
@Operation(
|
||||
summary = "Convert PDF to EPUB/AZW3",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.PDFWithPageNums;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -46,10 +45,7 @@ public class ConvertPDFToExcelController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/pdf/xlsx",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/pdf/xlsx", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Convert a PDF to an Excel spreadsheet (XLSX)",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -13,7 +13,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.util.PDFToFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -25,10 +24,7 @@ public class ConvertPDFToHtml {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/html",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/html")
|
||||
@Operation(
|
||||
summary = "Convert PDF to HTML",
|
||||
description =
|
||||
|
||||
+4
-17
@@ -22,7 +22,6 @@ import stirling.software.SPDF.model.api.converters.PdfToWordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -39,10 +38,7 @@ public class ConvertPDFToOffice {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final RuntimePathConfig runtimePathConfig;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/presentation",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/presentation")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Presentation format",
|
||||
description =
|
||||
@@ -57,10 +53,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "impress_pdf_import");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/text",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/text")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Text or RTF format",
|
||||
description =
|
||||
@@ -90,10 +83,7 @@ public class ConvertPDFToOffice {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/word",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/word")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Word document",
|
||||
description =
|
||||
@@ -107,10 +97,7 @@ public class ConvertPDFToOffice {
|
||||
return pdfToFile.processPdfToOfficeFormat(inputFile, outputFormat, "writer_pdf_import");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/xml",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/xml")
|
||||
@Operation(
|
||||
summary = "Convert PDF to XML",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -90,7 +90,6 @@ import stirling.software.SPDF.model.api.converters.PdfToPdfARequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
import stirling.software.common.util.ProcessExecutor.ProcessExecutorResult;
|
||||
@@ -573,10 +572,7 @@ public class ConvertPDFToPDFA {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/pdfa",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/pdfa")
|
||||
@Operation(
|
||||
summary = "Convert a PDF to a PDF/A or PDF/X",
|
||||
description =
|
||||
|
||||
+9
-24
@@ -32,7 +32,6 @@ import stirling.software.SPDF.model.json.PdfJsonMetadata;
|
||||
import stirling.software.SPDF.service.PdfJsonConversionService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.GeneralFile;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.JobOwnershipService;
|
||||
@@ -55,10 +54,7 @@ public class ConvertPdfJsonController {
|
||||
@Autowired(required = false)
|
||||
private JobOwnershipService jobOwnershipService;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = "multipart/form-data",
|
||||
value = "/pdf/text-editor",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Text Editor Format",
|
||||
description =
|
||||
@@ -96,10 +92,7 @@ public class ConvertPdfJsonController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = "multipart/form-data",
|
||||
value = "/text-editor/pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/text-editor/pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Convert Text Editor Format to PDF",
|
||||
@@ -130,10 +123,7 @@ public class ConvertPdfJsonController {
|
||||
return WebResponseUtils.pdfFileToWebResponse(tempOut, docName);
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = "multipart/form-data",
|
||||
value = "/pdf/text-editor/metadata",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = "multipart/form-data", value = "/pdf/text-editor/metadata")
|
||||
@Operation(
|
||||
summary = "Extract PDF metadata for text editor lazy loading",
|
||||
description =
|
||||
@@ -175,8 +165,7 @@ public class ConvertPdfJsonController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/pdf/text-editor/partial/{jobId}",
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Apply incremental edits from text editor to a cached PDF",
|
||||
@@ -195,6 +184,8 @@ public class ConvertPdfJsonController {
|
||||
|
||||
validateJobAccess(jobId);
|
||||
|
||||
byte[] pdfBytes = pdfJsonConversionService.exportUpdatedPages(jobId, document);
|
||||
|
||||
String baseName =
|
||||
(filename != null && !filename.isBlank())
|
||||
? FILE_EXTENSION_PATTERN
|
||||
@@ -206,18 +197,13 @@ public class ConvertPdfJsonController {
|
||||
.orElse("document");
|
||||
String docName = baseName.endsWith(".pdf") ? baseName : baseName + ".pdf";
|
||||
TempFile tempOut = tempFileManager.createManagedTempFile(".pdf");
|
||||
try (OutputStream os = Files.newOutputStream(tempOut.getPath())) {
|
||||
pdfJsonConversionService.exportUpdatedPages(jobId, document, os);
|
||||
} catch (Exception e) {
|
||||
tempOut.close();
|
||||
throw e;
|
||||
}
|
||||
try {
|
||||
return WebResponseUtils.pdfFileToWebResponse(tempOut, docName);
|
||||
Files.write(tempOut.getPath(), pdfBytes);
|
||||
} catch (Exception e) {
|
||||
tempOut.close();
|
||||
throw e;
|
||||
}
|
||||
return WebResponseUtils.pdfFileToWebResponse(tempOut, docName);
|
||||
}
|
||||
|
||||
@GetMapping(value = "/pdf/text-editor/page/{jobId}/{pageNumber}")
|
||||
@@ -280,8 +266,7 @@ public class ConvertPdfJsonController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/pdf/text-editor/clear-cache/{jobId}",
|
||||
consumes = MediaType.ALL_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.ALL_VALUE)
|
||||
@Operation(
|
||||
summary = "Clear cached PDF document for text editor",
|
||||
description =
|
||||
|
||||
+1
-4
@@ -49,10 +49,7 @@ public class ConvertPdfToVideoController {
|
||||
|
||||
// ffmpeg disabled due to raised CVEs
|
||||
/*
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/video",
|
||||
resourceWeight = ResourceWeight.XLARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/video")
|
||||
@Operation(
|
||||
summary = "Convert PDF to Video Slideshow",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -28,7 +28,6 @@ import stirling.software.SPDF.model.api.converters.SvgToPdfRequest;
|
||||
import stirling.software.SPDF.utils.SvgToPdf;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.SvgSanitizer;
|
||||
@@ -45,10 +44,7 @@ public class ConvertSvgToPDF {
|
||||
private final SvgSanitizer svgSanitizer;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/svg/pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/svg/pdf")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Convert SVG to PDF",
|
||||
|
||||
+1
-5
@@ -32,7 +32,6 @@ import stirling.software.SPDF.model.api.converters.UrlToPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -58,10 +57,7 @@ public class ConvertWebsiteToPDF {
|
||||
|
||||
private static final Pattern NUMERIC_HTML_ENTITY_PATTERN = Pattern.compile("&#(x?[0-9a-f]+);");
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/url/pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/url/pdf")
|
||||
@Operation(
|
||||
summary = "Convert a URL to a PDF",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -30,7 +30,6 @@ import stirling.software.SPDF.pdf.parser.PdfModels.TableFragment;
|
||||
import stirling.software.SPDF.pdf.parser.TabulaTableParser;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.WebResponseUtils;
|
||||
@@ -43,10 +42,7 @@ public class ExtractCSVController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TabulaTableParser tabulaTableParser;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/pdf/csv",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/pdf/csv", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@CsvConversionResponse
|
||||
@Operation(
|
||||
summary = "Extracts a CSV document from a PDF",
|
||||
|
||||
+2
-9
@@ -26,7 +26,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.converters.PdfVectorExportRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.ConvertApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.ProcessExecutor;
|
||||
@@ -46,10 +45,7 @@ public class PdfVectorExportController {
|
||||
private final TempFileManager tempFileManager;
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/vector/pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/vector/pdf")
|
||||
@Operation(
|
||||
summary = "Convert PostScript formats to PDF",
|
||||
description =
|
||||
@@ -96,10 +92,7 @@ public class PdfVectorExportController {
|
||||
return WebResponseUtils.pdfFileToWebResponse(outputTemp, outputName);
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/pdf/vector",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/pdf/vector")
|
||||
@Operation(
|
||||
summary = "Convert PDF to vector format",
|
||||
description =
|
||||
|
||||
+6
-17
@@ -27,7 +27,6 @@ import stirling.software.SPDF.model.api.filter.PageRotationRequest;
|
||||
import stirling.software.SPDF.model.api.filter.PageSizeRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.FilterApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
@@ -43,8 +42,7 @@ public class FilterController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-contains-text",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
value = "/filter-contains-text")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains set text, returns true if does",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -77,8 +75,7 @@ public class FilterController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-contains-image",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
value = "/filter-contains-image")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF contains an image",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -110,8 +107,7 @@ public class FilterController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-page-count",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
value = "/filter-page-count")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is greater, less or equal to a setPageCount",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -142,10 +138,7 @@ public class FilterController {
|
||||
: ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-page-size",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-page-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -182,10 +175,7 @@ public class FilterController {
|
||||
: ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-file-size",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/filter-file-size")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is a set file size",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
@@ -215,8 +205,7 @@ public class FilterController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/filter-page-rotation",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
value = "/filter-page-rotation")
|
||||
@Operation(
|
||||
summary = "Checks if a PDF is of a certain rotation",
|
||||
description = "Input:PDF Output:Boolean Type:SISO")
|
||||
|
||||
+1
-5
@@ -23,7 +23,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.misc.AddCommentsRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.comments.AnnotationLocation;
|
||||
import stirling.software.common.model.api.comments.StickyNoteSpec;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -67,10 +66,7 @@ public class AddCommentsController {
|
||||
private final PdfTextLocator pdfTextLocator;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/add-comments",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/add-comments", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add sticky-note comments to a PDF at specified positions or anchored text",
|
||||
|
||||
+1
-5
@@ -28,7 +28,6 @@ import stirling.software.SPDF.model.api.misc.RenameAttachmentRequest;
|
||||
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -49,10 +48,7 @@ public class AttachmentController {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/add-attachments",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-attachments")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add attachments to PDF",
|
||||
|
||||
+1
-5
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.ExtractHeaderRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.RegexPatternUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -40,10 +39,7 @@ public class AutoRenameController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/auto-rename",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/auto-rename")
|
||||
@Operation(
|
||||
summary = "Extract header from PDF file",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -38,7 +38,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.misc.AutoSplitPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -268,10 +267,7 @@ public class AutoSplitPdfController {
|
||||
return QR_DETECTION_DPI;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/auto-split-pdf",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/auto-split-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Auto split PDF pages into separate documents",
|
||||
|
||||
+1
-5
@@ -31,7 +31,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.RemoveBlankPagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
@@ -82,10 +81,7 @@ public class BlankPageController {
|
||||
return whitePixelPercentage >= whitePercent;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/remove-blanks",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-blanks")
|
||||
@Operation(
|
||||
summary = "Remove blank pages from a PDF file",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -49,7 +49,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.model.api.misc.OptimizePdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.LineArtConversionService;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -923,10 +922,7 @@ public class CompressController {
|
||||
return Math.min(9, currentLevel + 1);
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/compress-pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/compress-pdf")
|
||||
@Operation(
|
||||
summary = "Optimize PDF file",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -22,7 +22,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -39,10 +38,7 @@ public class DecompressPdfController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/decompress-pdf",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/decompress-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Decompress PDF streams",
|
||||
description = "Fully decompresses all PDF streams including text content")
|
||||
|
||||
+1
-3
@@ -32,7 +32,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.misc.ExtractImageScansRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
@@ -57,8 +56,7 @@ public class ExtractImageScansController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/extract-image-scans",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
value = "/extract-image-scans")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Extract image scans from an input file",
|
||||
|
||||
+1
-5
@@ -34,7 +34,6 @@ import stirling.software.SPDF.config.swagger.MultiFileResponse;
|
||||
import stirling.software.SPDF.model.api.PDFExtractImagesRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -50,10 +49,7 @@ public class ExtractImagesController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/extract-images",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/extract-images")
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Extract images from a PDF file",
|
||||
|
||||
+1
-5
@@ -27,7 +27,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.misc.FlattenRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
@@ -43,10 +42,7 @@ public class FlattenController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/flatten",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/flatten")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Flatten PDF form fields or full page",
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.misc.MetadataRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.PdfMetadataService;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -57,10 +56,7 @@ public class MetadataController {
|
||||
binder.registerCustomEditor(Map.class, "allRequestParams", new StringToMapPropertyEditor());
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/update-metadata",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/update-metadata")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Update metadata of a PDF file",
|
||||
|
||||
+1
-5
@@ -38,7 +38,6 @@ import stirling.software.SPDF.model.api.misc.ProcessPdfWithOcrRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.configuration.RuntimePathConfig;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -83,10 +82,7 @@ public class OCRController {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/ocr-pdf",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/ocr-pdf")
|
||||
@Operation(
|
||||
summary = "Process a PDF file with OCR",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -22,7 +22,6 @@ import stirling.software.SPDF.model.api.misc.OverlayImageRequest;
|
||||
import stirling.software.SPDF.utils.SvgOverlayUtil;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -37,10 +36,7 @@ public class OverlayImageController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/add-image",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-image")
|
||||
@Operation(
|
||||
summary = "Overlay image onto a PDF file",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -26,7 +26,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.misc.AddPageNumbersRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -40,10 +39,7 @@ public class PageNumbersController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/add-page-numbers",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/add-page-numbers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add page numbers to a PDF document",
|
||||
|
||||
+1
-5
@@ -25,7 +25,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.GeneralApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -42,10 +41,7 @@ public class RemoveImagesController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/remove-image-pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-image-pdf")
|
||||
@Operation(
|
||||
summary = "Remove images from PDF",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -19,7 +19,6 @@ import stirling.software.SPDF.config.EndpointConfiguration;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -47,10 +46,7 @@ public class RepairController {
|
||||
return endpointConfiguration.isGroupEnabled("qpdf");
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/repair",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/repair")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Repair a PDF file",
|
||||
|
||||
+1
-3
@@ -19,7 +19,6 @@ import stirling.software.SPDF.model.api.misc.ReplaceAndInvertColorRequest;
|
||||
import stirling.software.SPDF.service.misc.ReplaceAndInvertColorService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -34,8 +33,7 @@ public class ReplaceAndInvertColorController {
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/replace-invert-pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
value = "/replace-invert-pdf")
|
||||
@Operation(
|
||||
summary = "Replace-Invert Color PDF",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -46,7 +46,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.api.misc.ScannerEffectRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ApplicationContextProvider;
|
||||
@@ -560,10 +559,7 @@ public class ScannerEffectController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/scanner-effect",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/scanner-effect", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Operation(
|
||||
summary = "Apply scanner effect to PDF",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -21,7 +21,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.config.swagger.JavaScriptResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -35,10 +34,7 @@ public class ShowJavascript {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/show-javascript",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/show-javascript")
|
||||
@JavaScriptResponse
|
||||
@Operation(
|
||||
summary = "Grabs all JS from a PDF and returns a single JS file with all code",
|
||||
|
||||
+1
-5
@@ -46,7 +46,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.model.api.misc.AddStampRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -87,10 +86,7 @@ public class StampController {
|
||||
});
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/add-stamp",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-stamp")
|
||||
@Operation(
|
||||
summary = "Add stamp to a PDF file",
|
||||
description =
|
||||
|
||||
+1
-5
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.MiscApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -43,10 +42,7 @@ public class UnlockPDFFormsController {
|
||||
this.tempFileManager = tempFileManager;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/unlock-pdf-forms",
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/unlock-pdf-forms")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove read-only property from form fields",
|
||||
|
||||
+1
-5
@@ -26,7 +26,6 @@ import stirling.software.SPDF.model.PipelineResult;
|
||||
import stirling.software.SPDF.model.api.HandleDataRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.PipelineApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.PostHogService;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFile;
|
||||
@@ -50,10 +49,7 @@ public class PipelineController {
|
||||
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/handleData",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/handleData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@MultiFileResponse
|
||||
@Operation(
|
||||
summary = "Execute automated PDF processing pipeline",
|
||||
|
||||
+1
-3
@@ -75,7 +75,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.security.SignPDFWithCertRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.service.ServerCertificateServiceInterface;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -161,8 +160,7 @@ public class CertSignController {
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE
|
||||
},
|
||||
value = "/cert-sign",
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
value = "/cert-sign")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Sign PDF with a Digital Certificate",
|
||||
|
||||
+1
-5
@@ -56,7 +56,6 @@ import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||
import stirling.software.SPDF.service.VeraPDFService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -1062,10 +1061,7 @@ public class GetInfoOnPDF {
|
||||
return stats;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/get-info-on-pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/get-info-on-pdf")
|
||||
@Operation(
|
||||
summary = "Get comprehensive PDF information",
|
||||
description =
|
||||
|
||||
+2
-9
@@ -20,7 +20,6 @@ import stirling.software.SPDF.model.api.security.AddPasswordRequest;
|
||||
import stirling.software.SPDF.model.api.security.PDFPasswordRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -34,10 +33,7 @@ public class PasswordController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/remove-password",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-password")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove password from a PDF file",
|
||||
@@ -66,10 +62,7 @@ public class PasswordController {
|
||||
}
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/add-password",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-password")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add password to a PDF file",
|
||||
|
||||
+2
-9
@@ -59,7 +59,6 @@ import stirling.software.SPDF.utils.text.TextFinderUtils;
|
||||
import stirling.software.SPDF.utils.text.WidthCalculator;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.security.RedactionArea;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
@@ -102,10 +101,7 @@ public class RedactController {
|
||||
new StringToArrayListPropertyEditor<>(RedactionArea.class));
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/redact",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/redact", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
operationId = "redactPdfManual",
|
||||
@@ -498,10 +494,7 @@ public class RedactController {
|
||||
return pageNumbers;
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
value = "/auto-redact",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.LARGE_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/auto-redact", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Redact PDF automatically",
|
||||
|
||||
+1
-5
@@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.api.PDFFile;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -34,10 +33,7 @@ public class RemoveCertSignController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/remove-cert-sign",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/remove-cert-sign")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Remove digital signature from PDF",
|
||||
|
||||
+1
-5
@@ -39,7 +39,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.security.SanitizePdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
@@ -53,10 +52,7 @@ public class SanitizeController {
|
||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/sanitize-pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/sanitize-pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Sanitize a PDF file",
|
||||
|
||||
+1
-5
@@ -43,7 +43,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.security.TimestampPdfRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
@@ -79,10 +78,7 @@ public class TimestampController {
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/timestamp-pdf",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/timestamp-pdf")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add RFC 3161 document timestamp to a PDF",
|
||||
|
||||
+1
-3
@@ -42,7 +42,6 @@ import stirling.software.SPDF.model.api.security.SignatureValidationResult;
|
||||
import stirling.software.SPDF.service.CertificateValidationService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
@@ -75,8 +74,7 @@ public class ValidateSignatureController {
|
||||
+ " Input:PDF Output:JSON Type:SISO")
|
||||
@AutoJobPostMapping(
|
||||
value = "/validate-signature",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<List<SignatureValidationResult>> validateSignature(
|
||||
@ModelAttribute SignatureValidationRequest request) throws IOException {
|
||||
List<SignatureValidationResult> results = new ArrayList<>();
|
||||
|
||||
+1
-5
@@ -21,7 +21,6 @@ import stirling.software.SPDF.model.api.security.PDFVerificationResult;
|
||||
import stirling.software.SPDF.service.VeraPDFService;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.util.ExceptionUtils;
|
||||
|
||||
@SecurityApi
|
||||
@@ -38,10 +37,7 @@ public class VerifyPDFController {
|
||||
+ "Automatically detects PDF/A, PDF/UA-1, PDF/UA-2, and WTPDF standards "
|
||||
+ "from the document's XMP metadata and validates compliance. "
|
||||
+ "Input:PDF Output:JSON Type:SISO")
|
||||
@AutoJobPostMapping(
|
||||
value = "/verify-pdf",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
resourceWeight = ResourceWeight.SMALL_WEIGHT)
|
||||
@AutoJobPostMapping(value = "/verify-pdf", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ResponseEntity<List<PDFVerificationResult>> verifyPDF(
|
||||
@ModelAttribute PDFVerificationRequest request) {
|
||||
|
||||
|
||||
+1
-5
@@ -42,7 +42,6 @@ import stirling.software.SPDF.config.swagger.StandardPdfResponse;
|
||||
import stirling.software.SPDF.model.api.security.AddWatermarkRequest;
|
||||
import stirling.software.common.annotations.AutoJobPostMapping;
|
||||
import stirling.software.common.annotations.api.SecurityApi;
|
||||
import stirling.software.common.enumeration.ResourceWeight;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.GeneralUtils;
|
||||
import stirling.software.common.util.PdfUtils;
|
||||
@@ -69,10 +68,7 @@ public class WatermarkController {
|
||||
});
|
||||
}
|
||||
|
||||
@AutoJobPostMapping(
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
value = "/add-watermark",
|
||||
resourceWeight = ResourceWeight.MEDIUM_WEIGHT)
|
||||
@AutoJobPostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, value = "/add-watermark")
|
||||
@StandardPdfResponse
|
||||
@Operation(
|
||||
summary = "Add watermark to a PDF file",
|
||||
|
||||
@@ -6490,8 +6490,7 @@ public class PdfJsonConversionService {
|
||||
objectMapper.writeValue(out, pageFonts);
|
||||
}
|
||||
|
||||
public void exportUpdatedPages(String jobId, PdfJsonDocument updates, OutputStream outputStream)
|
||||
throws IOException {
|
||||
public byte[] exportUpdatedPages(String jobId, PdfJsonDocument updates) throws IOException {
|
||||
if (jobId == null || jobId.isBlank()) {
|
||||
throw new IllegalArgumentException("jobId is required for incremental export");
|
||||
}
|
||||
@@ -6514,8 +6513,7 @@ public class PdfJsonConversionService {
|
||||
log.debug(
|
||||
"Incremental export requested with no page updates; returning cached PDF for jobId {}",
|
||||
jobId);
|
||||
outputStream.write(cached.getPdfBytes());
|
||||
return;
|
||||
return cached.getPdfBytes();
|
||||
}
|
||||
|
||||
try (PDDocument document = pdfDocumentFactory.load(cached.getPdfBytes(), true)) {
|
||||
@@ -6582,8 +6580,7 @@ public class PdfJsonConversionService {
|
||||
log.debug(
|
||||
"Incremental export for jobId {} resulted in no page updates; returning cached PDF",
|
||||
jobId);
|
||||
outputStream.write(cached.getPdfBytes());
|
||||
return;
|
||||
return cached.getPdfBytes();
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
@@ -6606,7 +6603,7 @@ public class PdfJsonConversionService {
|
||||
"Incremental export complete for jobId {} (pages updated: {})",
|
||||
jobId,
|
||||
updatedPages.stream().map(i -> i + 1).sorted().toList());
|
||||
outputStream.write(updatedBytes);
|
||||
return updatedBytes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
package stirling.software.SPDF.bench;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font;
|
||||
import org.apache.pdfbox.pdmodel.font.Standard14Fonts;
|
||||
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
|
||||
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationWidget;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import stirling.software.common.util.FormUtils;
|
||||
import stirling.software.jpdfium.PdfDocument;
|
||||
import stirling.software.jpdfium.PdfSplit;
|
||||
|
||||
/**
|
||||
* Apples-to-apples heap + wall-clock benchmark for split: legacy PDFBox path (load + addPage per
|
||||
* range + save) vs JPDFium path (PdfSplit.extractPageRange + save).
|
||||
*
|
||||
* <p>Generates a single ~N-page PDF with one JPEG per page, then performs a "split into chunks of
|
||||
* 10" run with both backends. Samples heap every 25 ms during execution.
|
||||
*
|
||||
* <p>Run only when explicitly requested: -Dsplit.bench=true
|
||||
*/
|
||||
public final class SplitBenchmark {
|
||||
|
||||
private static final int PAGES = Integer.getInteger("split.bench.pages", 100);
|
||||
private static final int CHUNK = Integer.getInteger("split.bench.chunk", 10);
|
||||
private static final int IMAGE_W = Integer.getInteger("split.bench.imgW", 800);
|
||||
private static final int IMAGE_H = Integer.getInteger("split.bench.imgH", 600);
|
||||
private static final long SAMPLE_PERIOD_MS = 25L;
|
||||
|
||||
@Test
|
||||
void compareSplitMemoryFootprint() throws Exception {
|
||||
if (!Boolean.getBoolean("split.bench")) {
|
||||
System.out.println("Skipping SplitBenchmark (run with -Dsplit.bench=true)");
|
||||
return;
|
||||
}
|
||||
main(new String[0]);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Path workDir = Files.createTempDirectory("split-bench-");
|
||||
System.out.println("Work dir: " + workDir);
|
||||
System.out.printf(
|
||||
"Generating %d-page input PDF with %dx%d images per page...%n",
|
||||
PAGES, IMAGE_W, IMAGE_H);
|
||||
|
||||
Path input = workDir.resolve("input.pdf");
|
||||
long t0 = System.nanoTime();
|
||||
generateTestPdf(input, PAGES);
|
||||
long buildMs = (System.nanoTime() - t0) / 1_000_000;
|
||||
System.out.printf(
|
||||
" built %s: %,d KB (%d pages) in %,d ms%n%n",
|
||||
input.getFileName(), Files.size(input) / 1024, PAGES, buildMs);
|
||||
|
||||
// Warmup - prime classloaders, JIT, native lib.
|
||||
System.out.println("--- Warmup (results discarded) ---");
|
||||
runPdfBoxSplit(input, workDir.resolve("warmup-pdfbox"), CHUNK);
|
||||
runJpdfiumSplit(input, workDir.resolve("warmup-jpdfium"), CHUNK);
|
||||
forceGcQuiescence();
|
||||
System.out.println();
|
||||
|
||||
long baseline = sampleUsedHeapAfterGc();
|
||||
System.out.printf("Baseline heap (after GC, before split): %,d KB%n%n", baseline / 1024);
|
||||
|
||||
// PDFBox run
|
||||
System.out.println("--- PDFBox split (load + addPage + save per chunk) ---");
|
||||
Path outBoxDir = workDir.resolve("out-pdfbox");
|
||||
BenchResult pdfboxResult = profile(() -> runPdfBoxSplit(input, outBoxDir, CHUNK));
|
||||
pdfboxResult.printSummary("PDFBox", baseline);
|
||||
|
||||
forceGcQuiescence();
|
||||
System.out.println();
|
||||
|
||||
// JPDFium run
|
||||
System.out.println("--- JPDFium split (PdfSplit.extractPageRange + save) ---");
|
||||
Path outJpdDir = workDir.resolve("out-jpdfium");
|
||||
BenchResult jpdfiumResult = profile(() -> runJpdfiumSplit(input, outJpdDir, CHUNK));
|
||||
jpdfiumResult.printSummary("JPDFium", baseline);
|
||||
|
||||
// Compare
|
||||
System.out.println();
|
||||
System.out.println("=== Heap delta over baseline ===");
|
||||
long pdfboxDelta = pdfboxResult.peakHeapBytes - baseline;
|
||||
long jpdfiumDelta = jpdfiumResult.peakHeapBytes - baseline;
|
||||
double improvement =
|
||||
pdfboxDelta == 0 ? 0.0 : (100.0 * (pdfboxDelta - jpdfiumDelta) / pdfboxDelta);
|
||||
System.out.printf(" PDFBox : +%,d KB%n", pdfboxDelta / 1024);
|
||||
System.out.printf(" JPDFium : +%,d KB%n", jpdfiumDelta / 1024);
|
||||
System.out.printf(" JPDFium uses %.1f%% LESS heap than PDFBox%n", improvement);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("=== Wall-clock ===");
|
||||
System.out.printf(" PDFBox : %,d ms%n", pdfboxResult.wallMs);
|
||||
System.out.printf(" JPDFium : %,d ms%n", jpdfiumResult.wallMs);
|
||||
double speedup =
|
||||
jpdfiumResult.wallMs == 0
|
||||
? 0.0
|
||||
: ((double) pdfboxResult.wallMs / jpdfiumResult.wallMs);
|
||||
System.out.printf(" Speedup : %.2fx%n", speedup);
|
||||
|
||||
// Hybrid form-pruning sub-benchmark.
|
||||
// Quantifies the heap re-introduced by the PDFBox post-pass that
|
||||
// pruneOrphanedFormFields runs on each JPDFium-produced split.
|
||||
System.out.println();
|
||||
System.out.println("=== Hybrid form-pruning sub-benchmark ===");
|
||||
Path formInput = workDir.resolve("form-input.pdf");
|
||||
generateFormPdf(formInput, PAGES);
|
||||
System.out.printf(
|
||||
" built %s with AcroForm: %,d KB (%d pages)%n",
|
||||
formInput.getFileName(), Files.size(formInput) / 1024, PAGES);
|
||||
|
||||
runJpdfiumSplit(formInput, workDir.resolve("warmup-form-bare"), CHUNK);
|
||||
runJpdfiumSplitWithFormPrune(formInput, workDir.resolve("warmup-form-pruned"), CHUNK);
|
||||
forceGcQuiescence();
|
||||
|
||||
BenchResult bareForm =
|
||||
profile(() -> runJpdfiumSplit(formInput, workDir.resolve("out-form-bare"), CHUNK));
|
||||
bareForm.printSummary("JPDFium-only", sampleUsedHeapAfterGc());
|
||||
|
||||
forceGcQuiescence();
|
||||
BenchResult prunedForm =
|
||||
profile(
|
||||
() ->
|
||||
runJpdfiumSplitWithFormPrune(
|
||||
formInput, workDir.resolve("out-form-pruned"), CHUNK));
|
||||
prunedForm.printSummary("JPDFium+PDFBox-prune", sampleUsedHeapAfterGc());
|
||||
|
||||
long pruneOverhead = prunedForm.peakHeapBytes - bareForm.peakHeapBytes;
|
||||
long pruneTimeOverhead = prunedForm.wallMs - bareForm.wallMs;
|
||||
System.out.printf(
|
||||
" Hybrid prune overhead: +%,d KB heap, +%,d ms wall (%.1fx slower)%n",
|
||||
pruneOverhead / 1024,
|
||||
pruneTimeOverhead,
|
||||
bareForm.wallMs == 0 ? 0.0 : (double) prunedForm.wallMs / bareForm.wallMs);
|
||||
}
|
||||
|
||||
private static void runJpdfiumSplitWithFormPrune(Path input, Path outDir, int chunk)
|
||||
throws IOException {
|
||||
Files.createDirectories(outDir);
|
||||
try (PdfDocument source = PdfDocument.open(input)) {
|
||||
int total = source.pageCount();
|
||||
int index = 0;
|
||||
for (int start = 0; start < total; start += chunk) {
|
||||
int end = Math.min(start + chunk - 1, total - 1);
|
||||
Path raw = outDir.resolve("raw-" + (index + 1) + ".pdf");
|
||||
try (PdfDocument split = PdfSplit.extractPageRange(source, start, end)) {
|
||||
split.save(raw);
|
||||
}
|
||||
Path pruned = outDir.resolve("pruned-" + (++index) + ".pdf");
|
||||
try (PDDocument doc = org.apache.pdfbox.Loader.loadPDF(raw.toFile())) {
|
||||
FormUtils.pruneOrphanedFormFields(doc);
|
||||
doc.save(pruned.toFile());
|
||||
}
|
||||
Files.deleteIfExists(raw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateFormPdf(Path out, int pages) throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < pages; i++) {
|
||||
PDPage page = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(page);
|
||||
}
|
||||
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||
// One text field per page (orphan-prone scenario).
|
||||
for (int i = 0; i < pages; i++) {
|
||||
PDTextField field = new PDTextField(acroForm);
|
||||
field.setPartialName("field_" + i);
|
||||
acroForm.getFields().add(field);
|
||||
PDAnnotationWidget widget = new PDAnnotationWidget();
|
||||
widget.setRectangle(new PDRectangle(50, 50, 200, 30));
|
||||
widget.setPage(doc.getPage(i));
|
||||
doc.getPage(i).getAnnotations().add(widget);
|
||||
field.getWidgets().add(widget);
|
||||
}
|
||||
doc.save(out.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
private static void runPdfBoxSplit(Path input, Path outDir, int chunk) throws IOException {
|
||||
Files.createDirectories(outDir);
|
||||
try (PDDocument source = org.apache.pdfbox.Loader.loadPDF(input.toFile())) {
|
||||
int total = source.getNumberOfPages();
|
||||
int index = 0;
|
||||
for (int start = 0; start < total; start += chunk) {
|
||||
int end = Math.min(start + chunk - 1, total - 1);
|
||||
Set<Integer> keep = new HashSet<>();
|
||||
for (int p = start; p <= end; p++) keep.add(p);
|
||||
// Match the pre-jpdfium reload path: load fresh, removePage, save.
|
||||
try (PDDocument split = org.apache.pdfbox.Loader.loadPDF(input.toFile())) {
|
||||
for (int p = split.getNumberOfPages() - 1; p >= 0; p--) {
|
||||
if (!keep.contains(p)) split.removePage(p);
|
||||
}
|
||||
split.save(outDir.resolve("split-" + (++index) + ".pdf").toFile());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void runJpdfiumSplit(Path input, Path outDir, int chunk) throws IOException {
|
||||
Files.createDirectories(outDir);
|
||||
try (PdfDocument source = PdfDocument.open(input)) {
|
||||
int total = source.pageCount();
|
||||
int index = 0;
|
||||
for (int start = 0; start < total; start += chunk) {
|
||||
int end = Math.min(start + chunk - 1, total - 1);
|
||||
try (PdfDocument split = PdfSplit.extractPageRange(source, start, end)) {
|
||||
split.save(outDir.resolve("split-" + (++index) + ".pdf"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateTestPdf(Path out, int pages) throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
Random rng = new Random(42L);
|
||||
for (int i = 0; i < pages; i++) {
|
||||
PDPage page = new PDPage(PDRectangle.A4);
|
||||
doc.addPage(page);
|
||||
|
||||
byte[] jpegBytes = generateRandomJpeg(rng, IMAGE_W, IMAGE_H);
|
||||
PDImageXObject xobj = PDImageXObject.createFromByteArray(doc, jpegBytes, "img");
|
||||
|
||||
try (PDPageContentStream cs =
|
||||
new PDPageContentStream(
|
||||
doc, page, PDPageContentStream.AppendMode.APPEND, false)) {
|
||||
float pageW = page.getMediaBox().getWidth();
|
||||
float pageH = page.getMediaBox().getHeight();
|
||||
float imgW = pageW - 100;
|
||||
float imgH = imgW * IMAGE_H / IMAGE_W;
|
||||
float x = (pageW - imgW) / 2;
|
||||
float y = pageH - 80 - imgH;
|
||||
cs.drawImage(xobj, x, y, imgW, imgH);
|
||||
cs.beginText();
|
||||
cs.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA), 14);
|
||||
cs.newLineAtOffset(50, 30);
|
||||
cs.showText("Page " + (i + 1) + " bench fill");
|
||||
cs.endText();
|
||||
}
|
||||
}
|
||||
doc.save(out.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateRandomJpeg(Random rng, int w, int h) throws IOException {
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = img.createGraphics();
|
||||
try {
|
||||
float hue = rng.nextFloat();
|
||||
g.setColor(Color.getHSBColor(hue, 0.4f, 0.95f));
|
||||
g.fillRect(0, 0, w, h);
|
||||
for (int i = 0; i < 20; i++) {
|
||||
g.setColor(Color.getHSBColor(rng.nextFloat(), 0.6f, 0.5f + rng.nextFloat() * 0.5f));
|
||||
int r = 20 + rng.nextInt(80);
|
||||
int cx = rng.nextInt(w);
|
||||
int cy = rng.nextInt(h);
|
||||
g.fillOval(cx - r, cy - r, r * 2, r * 2);
|
||||
}
|
||||
g.setColor(Color.BLACK);
|
||||
g.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 32));
|
||||
g.drawString("bench image", 40, h - 40);
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, "jpg", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static BenchResult profile(ThrowingRunnable task) throws Exception {
|
||||
forceGcQuiescence();
|
||||
MemoryMXBean mem = ManagementFactory.getMemoryMXBean();
|
||||
AtomicLong peakHeap = new AtomicLong(0);
|
||||
AtomicLong peakNonHeap = new AtomicLong(0);
|
||||
AtomicBoolean stop = new AtomicBoolean(false);
|
||||
|
||||
Thread sampler =
|
||||
new Thread(
|
||||
() -> {
|
||||
while (!stop.get()) {
|
||||
MemoryUsage heap = mem.getHeapMemoryUsage();
|
||||
MemoryUsage nonHeap = mem.getNonHeapMemoryUsage();
|
||||
peakHeap.updateAndGet(p -> Math.max(p, heap.getUsed()));
|
||||
peakNonHeap.updateAndGet(p -> Math.max(p, nonHeap.getUsed()));
|
||||
try {
|
||||
Thread.sleep(SAMPLE_PERIOD_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
"split-bench-sampler");
|
||||
sampler.setDaemon(true);
|
||||
sampler.start();
|
||||
|
||||
long t0 = System.nanoTime();
|
||||
try {
|
||||
task.run();
|
||||
} finally {
|
||||
stop.set(true);
|
||||
sampler.join();
|
||||
}
|
||||
long wallMs = (System.nanoTime() - t0) / 1_000_000;
|
||||
return new BenchResult(peakHeap.get(), peakNonHeap.get(), wallMs);
|
||||
}
|
||||
|
||||
private static void forceGcQuiescence() {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
System.gc();
|
||||
try {
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long sampleUsedHeapAfterGc() {
|
||||
forceGcQuiescence();
|
||||
return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getUsed();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
void run() throws Exception;
|
||||
}
|
||||
|
||||
private record BenchResult(long peakHeapBytes, long peakNonHeapBytes, long wallMs) {
|
||||
void printSummary(String label, long baselineBytes) {
|
||||
System.out.printf(
|
||||
" %s peak heap : %,d KB (delta over baseline: +%,d KB)%n",
|
||||
label, peakHeapBytes / 1024, (peakHeapBytes - baselineBytes) / 1024);
|
||||
System.out.printf(" %s peak nonHeap : %,d KB%n", label, peakNonHeapBytes / 1024);
|
||||
System.out.printf(" %s wall-clock : %,d ms%n", label, wallMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
+38
-95
@@ -4,25 +4,17 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDAcroForm;
|
||||
import org.apache.pdfbox.pdmodel.interactive.form.PDTextField;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -31,12 +23,8 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPagesRequest;
|
||||
@@ -44,7 +32,6 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class SplitPDFControllerTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@@ -60,12 +47,6 @@ class SplitPDFControllerTest {
|
||||
String suffix = invocation.getArgument(0);
|
||||
return Files.createTempFile(tempDir, "test", suffix).toFile();
|
||||
});
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(File.class), eq(true)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(File.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
}
|
||||
|
||||
private byte[] createPdf(int numPages) throws IOException {
|
||||
@@ -79,47 +60,17 @@ class SplitPDFControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] createPdfWithForm(int numPages) throws IOException {
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
PDAcroForm acroForm = new PDAcroForm(doc);
|
||||
doc.getDocumentCatalog().setAcroForm(acroForm);
|
||||
PDTextField field = new PDTextField(acroForm);
|
||||
field.setPartialName("testField");
|
||||
acroForm.getFields().add(field);
|
||||
Path pdfPath = tempDir.resolve("input.pdf");
|
||||
doc.save(pdfPath.toFile());
|
||||
return Files.readAllBytes(pdfPath);
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> unzip(Resource zipResource) throws IOException {
|
||||
List<byte[]> entries = new ArrayList<>();
|
||||
try (ZipInputStream zis =
|
||||
new ZipInputStream(new ByteArrayInputStream(zipResource.getContentAsByteArray()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
entries.add(zis.readAllBytes());
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private int[] pageCountsOf(List<byte[]> entries) throws IOException {
|
||||
int[] counts = new int[entries.size()];
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
try (PDDocument doc = Loader.loadPDF(entries.get(i))) {
|
||||
counts[i] = doc.getNumberOfPages();
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
private void setupFactory() throws IOException {
|
||||
when(pdfDocumentFactory.load(any(File.class), eq(true)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
when(pdfDocumentFactory.load(any(File.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split 6-page PDF at page 3 into 2 parts")
|
||||
@DisplayName("Should split 6-page PDF at page 3")
|
||||
void shouldSplitAtPage3() throws Exception {
|
||||
byte[] pdfBytes = createPdf(6);
|
||||
MockMultipartFile file =
|
||||
@@ -130,12 +81,11 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("3");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(2);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(3, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -150,12 +100,11 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("1,2,3");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,16 +119,15 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("1");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(1);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split with multiple split points")
|
||||
@DisplayName("Should split with range notation")
|
||||
void shouldSplitWithRange() throws Exception {
|
||||
byte[] pdfBytes = createPdf(10);
|
||||
MockMultipartFile file =
|
||||
@@ -190,12 +138,11 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("3,7");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(3, 4, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -210,14 +157,13 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("2");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getHeaders().getContentType())
|
||||
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(2);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(2, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -232,12 +178,11 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("5");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(1);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(5);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -252,31 +197,29 @@ class SplitPDFControllerTest {
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("all");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(1, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split PDF with form fields and keep form-aware path")
|
||||
void shouldSplitFormPdf() throws Exception {
|
||||
byte[] pdfBytes = createPdfWithForm(4);
|
||||
@DisplayName("Should handle file without extension in original name")
|
||||
void shouldHandleFileWithoutExtension() throws Exception {
|
||||
byte[] pdfBytes = createPdf(2);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
"fileInput", "no_extension", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
|
||||
SplitPagesRequest request = new SplitPagesRequest();
|
||||
request.setFileInput(file);
|
||||
request.setPageNumbers("2");
|
||||
request.setPageNumbers("1");
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
setupFactory();
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(2);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(2, 2);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-60
@@ -4,18 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -34,11 +27,10 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import stirling.software.SPDF.model.api.SplitPdfByChaptersRequest;
|
||||
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
@@ -63,9 +55,6 @@ class SplitPdfByChaptersControllerTest {
|
||||
String suffix = inv.getArgument(0);
|
||||
return Files.createTempFile(tempDir, "test", suffix).toFile();
|
||||
});
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(File.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
}
|
||||
|
||||
private byte[] createPdfWithBookmarks(int numPages, String... chapterNames) throws IOException {
|
||||
@@ -94,29 +83,6 @@ class SplitPdfByChaptersControllerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> unzip(Resource zipResource) throws IOException {
|
||||
List<byte[]> entries = new ArrayList<>();
|
||||
try (ZipInputStream zis =
|
||||
new ZipInputStream(new ByteArrayInputStream(zipResource.getContentAsByteArray()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
entries.add(zis.readAllBytes());
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private int totalPagesOf(List<byte[]> entries) throws IOException {
|
||||
int total = 0;
|
||||
for (byte[] data : entries) {
|
||||
try (PDDocument doc = Loader.loadPDF(data)) {
|
||||
total += doc.getNumberOfPages();
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split PDF by chapters")
|
||||
void shouldSplitByChapters() throws Exception {
|
||||
@@ -131,12 +97,12 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(false);
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -153,12 +119,12 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(true);
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(2);
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,6 +163,10 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(false);
|
||||
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(
|
||||
inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> controller.splitPdf(request));
|
||||
}
|
||||
}
|
||||
@@ -215,12 +185,12 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(false);
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(1);
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -237,15 +207,14 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(true);
|
||||
request.setAllowDuplicates(false);
|
||||
|
||||
lenient()
|
||||
.when(pdfMetadataService.extractMetadataFromPdf(any(PDDocument.class)))
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
when(pdfMetadataService.extractMetadataFromPdf(any(PDDocument.class)))
|
||||
.thenReturn(new stirling.software.common.model.PdfMetadata());
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -262,12 +231,12 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(false);
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -284,11 +253,11 @@ class SplitPdfByChaptersControllerTest {
|
||||
request.setIncludeMetadata(false);
|
||||
request.setAllowDuplicates(true);
|
||||
|
||||
ResponseEntity<Resource> response = controller.splitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(MultipartFile.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF(((MultipartFile) inv.getArgument(0)).getBytes()));
|
||||
|
||||
var response = controller.splitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(5);
|
||||
assertThat(totalPagesOf(outputs)).isEqualTo(10);
|
||||
}
|
||||
}
|
||||
|
||||
+39
-111
@@ -4,18 +4,12 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
@@ -29,9 +23,6 @@ import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -42,7 +33,6 @@ import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||
import stirling.software.common.util.TempFileManager;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class SplitPdfBySizeControllerTest {
|
||||
|
||||
@TempDir Path tempDir;
|
||||
@@ -58,131 +48,69 @@ class SplitPdfBySizeControllerTest {
|
||||
String suffix = invocation.getArgument(0);
|
||||
return Files.createTempFile(tempDir, "test", suffix).toFile();
|
||||
});
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(File.class), eq(true)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
lenient()
|
||||
.when(pdfDocumentFactory.load(any(File.class)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
}
|
||||
|
||||
private byte[] createPdf(int numPages) throws IOException {
|
||||
@Test
|
||||
@DisplayName("Should split by page count successfully")
|
||||
void shouldSplitByPageCount() throws Exception {
|
||||
byte[] pdfBytes;
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < numPages; i++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
Path pdfPath = tempDir.resolve("input.pdf");
|
||||
doc.save(pdfPath.toFile());
|
||||
return Files.readAllBytes(pdfPath);
|
||||
pdfBytes = Files.readAllBytes(pdfPath);
|
||||
}
|
||||
}
|
||||
|
||||
private List<byte[]> unzip(Resource zipResource) throws IOException {
|
||||
List<byte[]> entries = new ArrayList<>();
|
||||
try (ZipInputStream zis =
|
||||
new ZipInputStream(new ByteArrayInputStream(zipResource.getContentAsByteArray()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
entries.add(zis.readAllBytes());
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private int[] pageCountsOf(List<byte[]> entries) throws IOException {
|
||||
int[] counts = new int[entries.size()];
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
try (PDDocument doc = Loader.loadPDF(entries.get(i))) {
|
||||
counts[i] = doc.getNumberOfPages();
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split by page count into 2-page chunks")
|
||||
void shouldSplitByPageCount() throws Exception {
|
||||
byte[] pdfBytes = createPdf(5);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
|
||||
request.setFileInput(file);
|
||||
request.setSplitType(1);
|
||||
request.setSplitType(1); // Page count
|
||||
request.setSplitValue("2");
|
||||
|
||||
ResponseEntity<Resource> response = controller.autoSplitPdf(request);
|
||||
when(pdfDocumentFactory.load(any(File.class), eq(true)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
|
||||
ResponseEntity<?> response = controller.autoSplitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getHeaders().getContentType())
|
||||
.isEqualTo(MediaType.APPLICATION_OCTET_STREAM);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(2, 2, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split by document count into 3 even documents")
|
||||
@DisplayName("Should split by document count successfully")
|
||||
void shouldSplitByDocCount() throws Exception {
|
||||
byte[] pdfBytes = createPdf(6);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
|
||||
request.setFileInput(file);
|
||||
request.setSplitType(2);
|
||||
request.setSplitValue("3");
|
||||
|
||||
ResponseEntity<Resource> response = controller.autoSplitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(2, 2, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split by document count distributing extras")
|
||||
void shouldSplitByDocCountWithRemainder() throws Exception {
|
||||
byte[] pdfBytes = createPdf(7);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
|
||||
request.setFileInput(file);
|
||||
request.setSplitType(2);
|
||||
request.setSplitValue("3");
|
||||
|
||||
ResponseEntity<Resource> response = controller.autoSplitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).hasSize(3);
|
||||
assertThat(pageCountsOf(outputs)).containsExactly(3, 2, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Should split by size into multiple files")
|
||||
void shouldSplitBySize() throws Exception {
|
||||
byte[] pdfBytes = createPdf(20);
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
|
||||
request.setFileInput(file);
|
||||
request.setSplitType(0);
|
||||
request.setSplitValue("3KB");
|
||||
|
||||
ResponseEntity<Resource> response = controller.autoSplitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
List<byte[]> outputs = unzip(response.getBody());
|
||||
assertThat(outputs).isNotEmpty();
|
||||
int total = 0;
|
||||
for (int count : pageCountsOf(outputs)) {
|
||||
total += count;
|
||||
byte[] pdfBytes;
|
||||
try (PDDocument doc = new PDDocument()) {
|
||||
for (int i = 0; i < 6; i++) {
|
||||
doc.addPage(new PDPage(PDRectangle.A4));
|
||||
}
|
||||
Path pdfPath = tempDir.resolve("input.pdf");
|
||||
doc.save(pdfPath.toFile());
|
||||
pdfBytes = Files.readAllBytes(pdfPath);
|
||||
}
|
||||
assertThat(total).isEqualTo(20);
|
||||
|
||||
MockMultipartFile file =
|
||||
new MockMultipartFile(
|
||||
"fileInput", "input.pdf", MediaType.APPLICATION_PDF_VALUE, pdfBytes);
|
||||
SplitPdfBySizeOrCountRequest request = new SplitPdfBySizeOrCountRequest();
|
||||
request.setFileInput(file);
|
||||
request.setSplitType(2); // Document count
|
||||
request.setSplitValue("3"); // Split into 3 docs (2 pages each)
|
||||
|
||||
when(pdfDocumentFactory.load(any(File.class), eq(true)))
|
||||
.thenAnswer(inv -> Loader.loadPDF((File) inv.getArgument(0)));
|
||||
when(pdfDocumentFactory.createNewDocumentBasedOnOldDocument(any(PDDocument.class)))
|
||||
.thenAnswer(inv -> new PDDocument());
|
||||
|
||||
ResponseEntity<?> response = controller.autoSplitPdf(request);
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ dependencies {
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-impl:$jwtVersion"
|
||||
runtimeOnly "io.jsonwebtoken:jjwt-jackson:$jwtVersion"
|
||||
runtimeOnly 'com.h2database:h2:2.3.232' // Don't upgrade h2database - file format incompatible with 2.4.x, would break existing user databases
|
||||
runtimeOnly 'org.postgresql:postgresql:42.7.11'
|
||||
runtimeOnly 'org.postgresql:postgresql:42.7.10'
|
||||
implementation('com.coveo:saml-client:5.0.0') {
|
||||
exclude group: 'org.opensaml', module: 'opensaml-core'
|
||||
}
|
||||
|
||||
-39
@@ -1,39 +0,0 @@
|
||||
package stirling.software.proprietary.audit;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import stirling.software.proprietary.config.AuditConfigurationProperties;
|
||||
import stirling.software.proprietary.security.config.EnterpriseEndpoint;
|
||||
|
||||
@Controller
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
@RequiredArgsConstructor
|
||||
@EnterpriseEndpoint
|
||||
public class AuditDashboardWebController {
|
||||
private final AuditConfigurationProperties auditConfig;
|
||||
|
||||
/** Display the audit dashboard. */
|
||||
@GetMapping("/audit")
|
||||
@Hidden
|
||||
public String showDashboard(Model model) {
|
||||
model.addAttribute("auditEnabled", auditConfig.isEnabled());
|
||||
model.addAttribute("auditLevel", auditConfig.getAuditLevel());
|
||||
model.addAttribute("auditLevelInt", auditConfig.getLevel());
|
||||
model.addAttribute("retentionDays", auditConfig.getRetentionDays());
|
||||
|
||||
// Add audit level enum values for display
|
||||
model.addAttribute("auditLevels", AuditLevel.values());
|
||||
|
||||
// Add audit event types for the dropdown
|
||||
model.addAttribute("auditEventTypes", AuditEventType.values());
|
||||
|
||||
return "audit/dashboard";
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user