mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
JDK25 - Integrate Stirling-Tools JPDFium for PDF operations (#6387)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
name: Backend build, format check, and coverage
|
name: Backend build, format check, and coverage
|
||||||
|
|
||||||
# Reusable workflow called from build.yml. Runs the full backend build matrix
|
# Reusable workflow called from build.yml. Runs the backend build matrix
|
||||||
# (JDK 21/25 × spring-security on/off), Spotless formatting check, JUnit, and
|
# (JDK 25 × spring-security on/off), Spotless formatting check, JUnit, and
|
||||||
# posts Jacoco coverage to PRs.
|
# posts Jacoco coverage to PRs.
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
jdk-version: [21, 25]
|
jdk-version: [25]
|
||||||
spring-security: [true, false]
|
spring-security: [true, false]
|
||||||
steps:
|
steps:
|
||||||
- name: Harden Runner
|
- name: Harden Runner
|
||||||
|
|||||||
@@ -94,21 +94,21 @@ jobs:
|
|||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
case "${{ github.event.inputs.platform }}" in
|
case "${{ github.event.inputs.platform }}" in
|
||||||
"windows")
|
"windows")
|
||||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-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"}]}' >> $GITHUB_OUTPUT
|
||||||
;;
|
;;
|
||||||
"macos")
|
"macos")
|
||||||
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal"}]}' >> $GITHUB_OUTPUT
|
echo 'matrix={"include":[{"platform":"macos-15","args":"--target universal-apple-darwin","name":"macos-universal","jpdfium_platforms":"darwin-arm64,darwin-x64"}]}' >> $GITHUB_OUTPUT
|
||||||
;;
|
;;
|
||||||
"linux")
|
"linux")
|
||||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
echo 'matrix={"include":[{"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
|
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
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
# For push/release events, build all platforms
|
# For push/release events, build all platforms
|
||||||
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
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-jars:
|
build-jars:
|
||||||
@@ -263,15 +263,17 @@ jobs:
|
|||||||
if: matrix.platform == 'macos-15'
|
if: matrix.platform == 'macos-15'
|
||||||
env:
|
env:
|
||||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||||
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||||
run: task desktop:jlink:universal-mac
|
run: task desktop:jlink:universal-mac
|
||||||
|
|
||||||
- name: Prepare desktop build
|
- name: Prepare desktop build
|
||||||
run: task desktop:prepare
|
|
||||||
env:
|
env:
|
||||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||||
DISABLE_ADDITIONAL_FEATURES: true
|
DISABLE_ADDITIONAL_FEATURES: true
|
||||||
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||||
|
run: task desktop:prepare
|
||||||
|
|
||||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||||
- name: Setup DigiCert KeyLocker
|
- name: Setup DigiCert KeyLocker
|
||||||
@@ -440,6 +442,10 @@ jobs:
|
|||||||
echo "Generated tauri.windows.conf.json (alias masked):"
|
echo "Generated tauri.windows.conf.json (alias masked):"
|
||||||
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/src-tauri/tauri.windows.conf.json
|
sed "s/${KEYPAIR_ALIAS}/***/g" ./frontend/src-tauri/tauri.windows.conf.json
|
||||||
|
|
||||||
|
- 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: Import release GPG signing key (Linux)
|
- name: Import release GPG signing key (Linux)
|
||||||
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
if: matrix.platform == 'ubuntu-22.04' && env.RELEASE_GPG_PRIVATE_KEY != '' && (github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.sign != 'false') || github.ref == 'refs/heads/V2-master')
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -47,12 +47,10 @@ jobs:
|
|||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
PLATFORM: ${{ inputs.platform }}
|
PLATFORM: ${{ inputs.platform }}
|
||||||
run: |
|
run: |
|
||||||
WINDOWS='{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}'
|
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"}'
|
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"}'
|
LINUX='{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64","jpdfium_platforms":"linux-x64"}'
|
||||||
|
|
||||||
# Resolve requested platform — populated by either workflow_dispatch
|
|
||||||
# or workflow_call inputs; both paths default to "all".
|
|
||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
windows) ENTRIES=("$WINDOWS") ;;
|
windows) ENTRIES=("$WINDOWS") ;;
|
||||||
macos) ENTRIES=("$MACOS") ;;
|
macos) ENTRIES=("$MACOS") ;;
|
||||||
@@ -113,10 +111,6 @@ jobs:
|
|||||||
toolchain: stable
|
toolchain: stable
|
||||||
targets: ${{ matrix.platform == 'macos-15' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
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)
|
- name: Set up x86_64 JDK 25 (macOS universal JRE)
|
||||||
if: matrix.platform == 'macos-15'
|
if: matrix.platform == 'macos-15'
|
||||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||||
@@ -143,21 +137,21 @@ jobs:
|
|||||||
- name: Setup Task
|
- name: Setup Task
|
||||||
uses: go-task/setup-task@3be4020d41929789a01026e0e427a4321ce0ad44 # v2.0.0
|
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
|
- name: Build universal macOS JRE
|
||||||
if: matrix.platform == 'macos-15'
|
if: matrix.platform == 'macos-15'
|
||||||
env:
|
env:
|
||||||
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
AARCH64_JAVA_HOME: ${{ env.JAVA_HOME }}
|
||||||
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||||
run: task desktop:jlink:universal-mac
|
run: task desktop:jlink:universal-mac
|
||||||
|
|
||||||
- name: Prepare desktop build
|
- name: Prepare desktop build
|
||||||
run: task desktop:prepare
|
|
||||||
env:
|
env:
|
||||||
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
MAVEN_USER: ${{ secrets.MAVEN_USER }}
|
||||||
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||||
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
MAVEN_PUBLIC_URL: ${{ secrets.MAVEN_PUBLIC_URL }}
|
||||||
DISABLE_ADDITIONAL_FEATURES: true
|
DISABLE_ADDITIONAL_FEATURES: true
|
||||||
|
JPDFIUM_PLATFORMS: ${{ matrix.jpdfium_platforms }}
|
||||||
|
run: task desktop:prepare
|
||||||
|
|
||||||
# DigiCert KeyLocker Setup (Cloud HSM)
|
# DigiCert KeyLocker Setup (Cloud HSM)
|
||||||
- name: Setup DigiCert KeyLocker
|
- name: Setup DigiCert KeyLocker
|
||||||
@@ -270,6 +264,10 @@ jobs:
|
|||||||
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> $GITHUB_ENV
|
||||||
echo "Certificate imported successfully."
|
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)
|
- name: Check DMG creation dependencies (macOS only)
|
||||||
if: matrix.platform == 'macos-15'
|
if: matrix.platform == 'macos-15'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
+20
-3
@@ -3,6 +3,22 @@ version: '3'
|
|||||||
vars:
|
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"
|
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:
|
tasks:
|
||||||
prepare:
|
prepare:
|
||||||
desc: "Prepare desktop build dependencies"
|
desc: "Prepare desktop build dependencies"
|
||||||
@@ -71,15 +87,16 @@ tasks:
|
|||||||
deps: [jlink:jar, jlink:runtime]
|
deps: [jlink:jar, jlink:runtime]
|
||||||
|
|
||||||
jlink:jar:
|
jlink:jar:
|
||||||
desc: "Build backend JAR for Tauri bundling"
|
desc: "Build backend JAR for Tauri bundling (host-OS natives only by default)"
|
||||||
run: once
|
run: once
|
||||||
dir: ..
|
dir: ..
|
||||||
env:
|
env:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
cmds:
|
cmds:
|
||||||
- cmd: cmd /c gradlew.bat bootJar --no-daemon
|
- echo "Building bootJar with JPDFium natives for {{.JPDFIUM_PLATFORMS}}"
|
||||||
|
- cmd: cmd /c gradlew.bat bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||||
platforms: [windows]
|
platforms: [windows]
|
||||||
- cmd: ./gradlew bootJar --no-daemon
|
- cmd: ./gradlew bootJar --no-daemon -PjpdfiumPlatforms={{.JPDFIUM_PLATFORMS}}
|
||||||
platforms: [linux, darwin]
|
platforms: [linux, darwin]
|
||||||
- mkdir -p frontend/src-tauri/libs
|
- mkdir -p frontend/src-tauri/libs
|
||||||
- cp app/core/build/libs/stirling-pdf-*.jar 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
|
## Important Notes
|
||||||
|
|
||||||
- **Java Version**: Minimum JDK 21, supports and recommends JDK 25
|
- **Java Version**: Requires JDK 25.
|
||||||
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
- **Lombok**: Used extensively - ensure IDE plugin is installed
|
||||||
- **File Persistence**:
|
- **File Persistence**:
|
||||||
- **Backend**: Designed to be stateless - files are processed in memory/temp locations only
|
- **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:
|
**Stirling 2.0** is built using:
|
||||||
|
|
||||||
**Backend:**
|
**Backend:**
|
||||||
- Spring Boot (Java 21+, JDK 25 recommended)
|
- Spring Boot (requires JDK 25)
|
||||||
- PDFBox for core PDF operations
|
- PDFBox for core PDF operations
|
||||||
- LibreOffice for document conversions
|
- LibreOffice for document conversions
|
||||||
- qpdf for PDF optimization
|
- 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)
|
- [Task](https://taskfile.dev/installation/) — unified command runner (recommended)
|
||||||
- Docker
|
- Docker
|
||||||
- Git
|
- Git
|
||||||
- Java JDK 21 or later (JDK 25 recommended)
|
- Java JDK 25
|
||||||
- Node.js 18+ and npm (required for frontend development)
|
- Node.js 18+ and npm (required for frontend development)
|
||||||
- Gradle 7.0 or later (Included within the repo)
|
- Gradle 7.0 or later (Included within the repo)
|
||||||
- [uv](https://docs.astral.sh/uv/) — Python package manager (required for engine development)
|
- [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
|
cd Stirling-PDF
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install Docker and JDK 21 (or JDK 25 recommended) if not already installed.
|
2. Install Docker and JDK 25 if not already installed.
|
||||||
|
|
||||||
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
3. Install a recommended Java IDE such as Eclipse, IntelliJ, or VSCode
|
||||||
1. Only VSCode
|
1. Only VSCode
|
||||||
|
|||||||
@@ -60,6 +60,24 @@ dependencies {
|
|||||||
exclude group: 'com.google.code.gson', module: 'gson'
|
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)
|
// ArchUnit: enforces module dependency direction (see ArchitectureTest)
|
||||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.2'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,7 +162,8 @@ bootJar {
|
|||||||
manifest {
|
manifest {
|
||||||
attributes(
|
attributes(
|
||||||
'Implementation-Title': 'Stirling-PDF',
|
'Implementation-Title': 'Stirling-PDF',
|
||||||
'Implementation-Version': project.version
|
'Implementation-Version': project.version,
|
||||||
|
'Enable-Native-Access': 'ALL-UNNAMED'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-64
@@ -3,13 +3,14 @@ package stirling.software.SPDF.controller.api;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.apache.pdfbox.multipdf.PDFMergerUtility;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
import org.apache.pdfbox.pdmodel.PDDocumentInformation;
|
||||||
@@ -47,6 +48,11 @@ import stirling.software.common.util.PdfErrorUtils;
|
|||||||
import stirling.software.common.util.TempFile;
|
import stirling.software.common.util.TempFile;
|
||||||
import stirling.software.common.util.TempFileManager;
|
import stirling.software.common.util.TempFileManager;
|
||||||
import stirling.software.common.util.WebResponseUtils;
|
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
|
@GeneralApi
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -57,7 +63,6 @@ public class MergeController {
|
|||||||
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
private final TempFileManager tempFileManager;
|
private final TempFileManager tempFileManager;
|
||||||
|
|
||||||
// Merges a list of PDDocument objects into a single PDDocument
|
|
||||||
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
|
||||||
PDDocument mergedDoc = pdfDocumentFactory.createNewDocument();
|
PDDocument mergedDoc = pdfDocumentFactory.createNewDocument();
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
@@ -76,11 +81,8 @@ 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(
|
private static MultipartFile[] reorderFilesByProvidedOrder(
|
||||||
MultipartFile[] files, String fileOrder) {
|
MultipartFile[] files, String fileOrder) {
|
||||||
// Split by various line endings and trim each entry
|
|
||||||
String[] desired =
|
String[] desired =
|
||||||
stirling.software.common.util.RegexPatternUtils.getInstance()
|
stirling.software.common.util.RegexPatternUtils.getInstance()
|
||||||
.getNewlineSplitPattern()
|
.getNewlineSplitPattern()
|
||||||
@@ -107,7 +109,6 @@ public class MergeController {
|
|||||||
return ordered.toArray(new MultipartFile[0]);
|
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) {
|
private Comparator<MultipartFile> getSortComparator(String sortType) {
|
||||||
return switch (sortType) {
|
return switch (sortType) {
|
||||||
case "byFileName" ->
|
case "byFileName" ->
|
||||||
@@ -155,18 +156,16 @@ public class MergeController {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
case "orderProvided" -> (file1, file2) -> 0; // Default is the order provided
|
case "orderProvided" -> (file1, file2) -> 0;
|
||||||
default -> (file1, file2) -> 0; // Default is the order provided
|
default -> (file1, file2) -> 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse client file IDs from JSON string
|
|
||||||
private String[] parseClientFileIds(String clientFileIds) {
|
private String[] parseClientFileIds(String clientFileIds) {
|
||||||
if (clientFileIds == null || clientFileIds.trim().isEmpty()) {
|
if (clientFileIds == null || clientFileIds.trim().isEmpty()) {
|
||||||
return new String[0];
|
return new String[0];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// Simple JSON array parsing - remove brackets and split by comma
|
|
||||||
String trimmed = clientFileIds.trim();
|
String trimmed = clientFileIds.trim();
|
||||||
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
String inside = trimmed.substring(1, trimmed.length() - 1).trim();
|
String inside = trimmed.substring(1, trimmed.length() - 1).trim();
|
||||||
@@ -186,39 +185,29 @@ public class MergeController {
|
|||||||
return new String[0];
|
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) {
|
private void addTableOfContents(PDDocument mergedDocument, MultipartFile[] files) {
|
||||||
// Create the document outline
|
|
||||||
PDDocumentOutline outline = new PDDocumentOutline();
|
PDDocumentOutline outline = new PDDocumentOutline();
|
||||||
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
|
mergedDocument.getDocumentCatalog().setDocumentOutline(outline);
|
||||||
|
|
||||||
int pageIndex = 0; // Current page index in the merged document
|
int pageIndex = 0;
|
||||||
|
|
||||||
// Iterate through the original files
|
|
||||||
for (MultipartFile file : files) {
|
for (MultipartFile file : files) {
|
||||||
// Get the filename without extension to use as bookmark title
|
|
||||||
String filename = file.getOriginalFilename();
|
String filename = file.getOriginalFilename();
|
||||||
String title = GeneralUtils.removeExtension(filename);
|
String title = GeneralUtils.removeExtension(filename);
|
||||||
|
|
||||||
// Create an outline item for this file
|
|
||||||
PDOutlineItem item = new PDOutlineItem();
|
PDOutlineItem item = new PDOutlineItem();
|
||||||
item.setTitle(title);
|
item.setTitle(title);
|
||||||
|
|
||||||
// Set the destination to the first page of this file in the merged document
|
|
||||||
if (pageIndex < mergedDocument.getNumberOfPages()) {
|
if (pageIndex < mergedDocument.getNumberOfPages()) {
|
||||||
PDPage page = mergedDocument.getPage(pageIndex);
|
PDPage page = mergedDocument.getPage(pageIndex);
|
||||||
item.setDestination(page);
|
item.setDestination(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the item to the outline
|
|
||||||
outline.addLast(item);
|
outline.addLast(item);
|
||||||
|
|
||||||
// Increment page index for the next file
|
|
||||||
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
try (PDDocument doc = pdfDocumentFactory.load(file)) {
|
||||||
pageIndex += doc.getNumberOfPages();
|
pageIndex += doc.getNumberOfPages();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ExceptionUtils.logException("document loading for TOC generation", e);
|
ExceptionUtils.logException("document loading for TOC generation", e);
|
||||||
pageIndex++; // Increment by at least one if we can't determine page count
|
pageIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +225,6 @@ public class MergeController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to XMP metadata if Info dates are missing
|
|
||||||
PDMetadata metadata = doc.getDocumentCatalog().getMetadata();
|
PDMetadata metadata = doc.getDocumentCatalog().getMetadata();
|
||||||
if (metadata != null) {
|
if (metadata != null) {
|
||||||
try (InputStream is = metadata.createInputStream()) {
|
try (InputStream is = metadata.createInputStream()) {
|
||||||
@@ -287,7 +275,7 @@ public class MergeController {
|
|||||||
@ModelAttribute MergePdfsRequest request,
|
@ModelAttribute MergePdfsRequest request,
|
||||||
@RequestParam(value = "fileOrder", required = false) String fileOrder)
|
@RequestParam(value = "fileOrder", required = false) String fileOrder)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
List<File> filesToDelete = new ArrayList<>(); // List of temporary files to delete
|
List<File> filesToDelete = new ArrayList<>();
|
||||||
TempFile outputTempFile = null;
|
TempFile outputTempFile = null;
|
||||||
|
|
||||||
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
boolean removeCertSign = Boolean.TRUE.equals(request.getRemoveCertSign());
|
||||||
@@ -298,48 +286,35 @@ public class MergeController {
|
|||||||
files = new MultipartFile[0];
|
files = new MultipartFile[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// If front-end provided explicit visible order, honor it and override backend sorting
|
|
||||||
if (fileOrder != null && !fileOrder.isBlank()) {
|
if (fileOrder != null && !fileOrder.isBlank()) {
|
||||||
log.info("Reordering files based on fileOrder parameter");
|
log.info("Reordering files based on fileOrder parameter");
|
||||||
files = reorderFilesByProvidedOrder(files, fileOrder);
|
files = reorderFilesByProvidedOrder(files, fileOrder);
|
||||||
} else {
|
} else {
|
||||||
log.info("Sorting files based on sortType: {}", request.getSortType());
|
log.info("Sorting files based on sortType: {}", request.getSortType());
|
||||||
Arrays.sort(
|
Arrays.sort(files, getSortComparator(request.getSortType()));
|
||||||
files,
|
|
||||||
getSortComparator(
|
|
||||||
request.getSortType())); // Sort files based on requested sort type
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try (TempFile mt = new TempFile(tempFileManager, ".pdf")) {
|
try (TempFile mt = new TempFile(tempFileManager, ".pdf")) {
|
||||||
|
|
||||||
PDFMergerUtility mergerUtility = new PDFMergerUtility();
|
List<Path> inputPaths = new ArrayList<>(files.length);
|
||||||
long totalSize = 0;
|
|
||||||
List<Integer> invalidIndexes = new ArrayList<>();
|
List<Integer> invalidIndexes = new ArrayList<>();
|
||||||
for (int index = 0; index < files.length; index++) {
|
for (int index = 0; index < files.length; index++) {
|
||||||
MultipartFile multipartFile = files[index];
|
MultipartFile multipartFile = files[index];
|
||||||
totalSize += multipartFile.getSize();
|
File tempFile = tempFileManager.convertMultipartFileToFile(multipartFile);
|
||||||
File tempFile =
|
filesToDelete.add(tempFile);
|
||||||
tempFileManager.convertMultipartFileToFile(
|
inputPaths.add(tempFile.toPath());
|
||||||
multipartFile); // Convert MultipartFile to File
|
|
||||||
filesToDelete.add(tempFile); // Add temp file to the list for later deletion
|
|
||||||
|
|
||||||
// Pre-validate each PDF so we can report which one(s) are broken
|
try (PdfDocument ignored = PdfDocument.open(tempFile.toPath())) {
|
||||||
// Use the original MultipartFile to avoid deleting the tempFile during validation
|
} catch (Exception e) {
|
||||||
try (PDDocument ignored = pdfDocumentFactory.load(multipartFile)) {
|
|
||||||
// OK
|
|
||||||
} catch (IOException e) {
|
|
||||||
ExceptionUtils.logException("PDF pre-validate", e);
|
ExceptionUtils.logException("PDF pre-validate", e);
|
||||||
invalidIndexes.add(index);
|
invalidIndexes.add(index);
|
||||||
}
|
}
|
||||||
mergerUtility.addSource(tempFile); // Add source file to the merger utility
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mergerUtility.setDestinationFileName(mt.getFile().getAbsolutePath());
|
int[] pageCounts;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mergerUtility.mergeDocuments(
|
pageCounts =
|
||||||
pdfDocumentFactory.getStreamCacheFunction(
|
mergeWithJpdfium(inputPaths, files, generateToc, mt.getFile().toPath());
|
||||||
totalSize)); // Merge the documents
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ExceptionUtils.logException("PDF merge", e);
|
ExceptionUtils.logException("PDF merge", e);
|
||||||
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
if (PdfErrorUtils.isCorruptedPdfError(e)) {
|
||||||
@@ -348,10 +323,26 @@ public class MergeController {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the merged PDF document and operate on it inside try-with-resources
|
boolean sigFlattenNeeded = false;
|
||||||
try (PDDocument mergedDocument = pdfDocumentFactory.load(mt.getFile())) {
|
if (removeCertSign) {
|
||||||
// Remove signatures if removeCertSign is true
|
try (PdfDocument check = PdfDocument.open(mt.getFile().toPath())) {
|
||||||
if (removeCertSign) {
|
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())) {
|
||||||
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
PDDocumentCatalog catalog = mergedDocument.getDocumentCatalog();
|
||||||
PDAcroForm acroForm = catalog.getAcroForm();
|
PDAcroForm acroForm = catalog.getAcroForm();
|
||||||
if (acroForm != null) {
|
if (acroForm != null) {
|
||||||
@@ -359,24 +350,26 @@ public class MergeController {
|
|||||||
acroForm.getFields().stream()
|
acroForm.getFields().stream()
|
||||||
.filter(PDSignatureField.class::isInstance)
|
.filter(PDSignatureField.class::isInstance)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
if (!fieldsToRemove.isEmpty()) {
|
if (!fieldsToRemove.isEmpty()) {
|
||||||
acroForm.flatten(
|
acroForm.flatten(fieldsToRemove, false);
|
||||||
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");
|
outputTempFile = new TempFile(tempFileManager, ".pdf");
|
||||||
try {
|
try {
|
||||||
mergedDocument.save(outputTempFile.getFile());
|
Files.copy(
|
||||||
|
mt.getFile().toPath(),
|
||||||
|
outputTempFile.getFile().toPath(),
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
outputTempFile.close();
|
outputTempFile.close();
|
||||||
outputTempFile = null;
|
outputTempFile = null;
|
||||||
@@ -395,7 +388,7 @@ public class MergeController {
|
|||||||
throw ex;
|
throw ex;
|
||||||
} finally {
|
} finally {
|
||||||
for (File file : filesToDelete) {
|
for (File file : filesToDelete) {
|
||||||
tempFileManager.deleteTempFile(file); // Delete temporary files
|
tempFileManager.deleteTempFile(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,4 +398,113 @@ public class MergeController {
|
|||||||
|
|
||||||
return WebResponseUtils.pdfFileToWebResponse(outputTempFile, mergedFileName);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-6
@@ -31,12 +31,12 @@ ext {
|
|||||||
googleJavaFormatVersion = "1.28.0"
|
googleJavaFormatVersion = "1.28.0"
|
||||||
logback = "1.5.32"
|
logback = "1.5.32"
|
||||||
// junit-platform-launcher version managed by Spring Boot BOM
|
// junit-platform-launcher version managed by Spring Boot BOM
|
||||||
modernJavaVersion = 21
|
modernJavaVersion = 25
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(project.findProperty('javaVersion')?.toString() ?: '25')
|
languageVersion = JavaLanguageVersion.of(project.findProperty('javaVersion')?.toString() ?: '25')
|
||||||
}
|
}
|
||||||
@@ -158,8 +158,8 @@ subprojects {
|
|||||||
apply plugin: 'jacoco'
|
apply plugin: 'jacoco'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_25
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_25
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(25)
|
languageVersion = JavaLanguageVersion.of(25)
|
||||||
}
|
}
|
||||||
@@ -444,7 +444,8 @@ subprojects {
|
|||||||
"-XX:G1HeapRegionSize=4m",
|
"-XX:G1HeapRegionSize=4m",
|
||||||
"-XX:+ExplicitGCInvokesConcurrent",
|
"-XX:+ExplicitGCInvokesConcurrent",
|
||||||
"-XX:+UseStringDeduplication",
|
"-XX:+UseStringDeduplication",
|
||||||
"-XX:+UseCompactObjectHeaders"
|
"-XX:+UseCompactObjectHeaders",
|
||||||
|
"--enable-native-access=ALL-UNNAMED"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Sign every .dylib inside the bootJar's JPDFium native jars.
|
||||||
|
# Requires APPLE_SIGNING_IDENTITY set to a Developer ID identity in the keychain.
|
||||||
|
#
|
||||||
|
# Usage: sign-jpdfium-dylibs-in-bootjar.sh [path/to/stirling-pdf-*.jar]
|
||||||
|
|
||||||
|
set -u
|
||||||
|
|
||||||
|
echo "sign-jpdfium-dylibs-in-bootjar.sh: start ($(uname -s) $(uname -m))"
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Darwin*) ;;
|
||||||
|
*) echo "Not macOS, skipping"; exit 0;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then
|
||||||
|
echo "APPLE_SIGNING_IDENTITY not set; skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! command -v codesign >/dev/null 2>&1; then
|
||||||
|
echo "codesign not on PATH; skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if ! command -v jar >/dev/null 2>&1; then
|
||||||
|
echo "jar not on PATH (need a JDK setup-action earlier); skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BOOTJARS=()
|
||||||
|
if [ -n "${1:-}" ]; then
|
||||||
|
BOOTJARS+=("$1")
|
||||||
|
else
|
||||||
|
for cand in app/core/build/libs/stirling-pdf-*.jar \
|
||||||
|
frontend/src-tauri/libs/stirling-pdf-*.jar; do
|
||||||
|
[ -f "$cand" ] || continue
|
||||||
|
BOOTJARS+=("$cand")
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ "${#BOOTJARS[@]:-0}" = 0 ]; then
|
||||||
|
echo "bootJar not found (expected app/core/build/libs/stirling-pdf-*.jar" \
|
||||||
|
"or frontend/src-tauri/libs/stirling-pdf-*.jar)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for BOOTJAR in "${BOOTJARS[@]}"; do
|
||||||
|
BOOTJAR=$(cd "$(dirname "$BOOTJAR")" && pwd)/$(basename "$BOOTJAR")
|
||||||
|
echo ""
|
||||||
|
echo "=== Target bootJar: $BOOTJAR ($(du -h "$BOOTJAR" | cut -f1)) ==="
|
||||||
|
|
||||||
|
WORK=$(mktemp -d)
|
||||||
|
# shellcheck disable=SC2064
|
||||||
|
trap "rm -rf '$WORK'" EXIT
|
||||||
|
|
||||||
|
NATIVE_JAR_PATHS=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
NATIVE_JAR_PATHS+=("$line")
|
||||||
|
done < <(jar tf "$BOOTJAR" \
|
||||||
|
| grep -E '^BOOT-INF/lib/jpdfium-natives-darwin-(x64|arm64)-.*\.jar$' || true)
|
||||||
|
|
||||||
|
if [ "${#NATIVE_JAR_PATHS[@]:-0}" = 0 ]; then
|
||||||
|
echo " No JPDFium darwin natives in this bootJar; skipping"
|
||||||
|
rm -rf "$WORK"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
( cd "$WORK" && jar xf "$BOOTJAR" ${NATIVE_JAR_PATHS[@]+"${NATIVE_JAR_PATHS[@]}"} ) \
|
||||||
|
|| { echo "jar xf failed to extract natives jars" >&2; exit 1; }
|
||||||
|
|
||||||
|
ANY_SIGNED=0
|
||||||
|
for nat_jar in "$WORK/BOOT-INF/lib"/jpdfium-natives-darwin-*.jar; do
|
||||||
|
[ -f "$nat_jar" ] || continue
|
||||||
|
base=$(basename "$nat_jar")
|
||||||
|
echo " Processing $base"
|
||||||
|
|
||||||
|
exp_dir="$WORK/${base%.jar}.expanded"
|
||||||
|
mkdir -p "$exp_dir"
|
||||||
|
( cd "$exp_dir" && jar xf "$nat_jar" )
|
||||||
|
|
||||||
|
signed=0
|
||||||
|
while IFS= read -r dylib; do
|
||||||
|
codesign --force --sign "$APPLE_SIGNING_IDENTITY" \
|
||||||
|
--options runtime --timestamp "$dylib" 2>&1 | sed 's/^/ /'
|
||||||
|
signed=$((signed + 1))
|
||||||
|
done < <(find "$exp_dir" -name '*.dylib' -type f)
|
||||||
|
|
||||||
|
if [ "$signed" = 0 ]; then
|
||||||
|
echo " (no .dylibs found)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo " signed $signed dylib(s)"
|
||||||
|
|
||||||
|
rm -f "$nat_jar"
|
||||||
|
( cd "$exp_dir" && jar cfM0 "$nat_jar" . )
|
||||||
|
ANY_SIGNED=1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ANY_SIGNED" = 0 ]; then
|
||||||
|
echo " No .dylibs signed; skipping update"
|
||||||
|
rm -rf "$WORK"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# `jar uf` always DEFLATEs; Spring Boot's nested-jar loader only reads
|
||||||
|
# STORED entries. Use `zip -0` to update in place with no compression.
|
||||||
|
( cd "$WORK" && zip -q0 "$BOOTJAR" ${NATIVE_JAR_PATHS[@]+"${NATIVE_JAR_PATHS[@]}"} ) \
|
||||||
|
|| { echo "zip update failed" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo " Updated: $BOOTJAR ($(du -h "$BOOTJAR" | cut -f1))"
|
||||||
|
rm -rf "$WORK"
|
||||||
|
done
|
||||||
+4
-2
@@ -442,8 +442,10 @@ compare_file_lists() {
|
|||||||
echo "New files created during test:"
|
echo "New files created during test:"
|
||||||
cat "${diff_file}.added" | sed 's/^> //'
|
cat "${diff_file}.added" | sed 's/^> //'
|
||||||
|
|
||||||
# Check for tmp files
|
# Exclude JPDFium native cache (deleteOnExit-registered, not a leak).
|
||||||
grep -i "tmp\|temp" "${diff_file}.added" > "${diff_file}.tmp" || true
|
grep -i "tmp\|temp" "${diff_file}.added" \
|
||||||
|
| grep -v '/jpdfium-[0-9]\+/' \
|
||||||
|
> "${diff_file}.tmp" || true
|
||||||
if [ -s "${diff_file}.tmp" ]; then
|
if [ -s "${diff_file}.tmp" ]; then
|
||||||
echo "WARNING: Temporary files detected:"
|
echo "WARNING: Temporary files detected:"
|
||||||
cat "${diff_file}.tmp"
|
cat "${diff_file}.tmp"
|
||||||
|
|||||||
Reference in New Issue
Block a user