Compare commits

..

11 Commits

Author SHA1 Message Date
Anthony Stirling
c2062c19ab Merge remote-tracking branch 'origin/main' into bug/remember-me 2024-11-15 21:30:14 +00:00
Anthony Stirling
6760fddd6f Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
2024-11-15 21:26:51 +00:00
a
20a75d577d Merge branch 'bug/remember-me' of git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me 2024-11-15 13:13:34 +00:00
Anthony Stirling
559581c59d Merge branch 'bug/remember-me' of
git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me
2024-11-15 13:12:20 +00:00
Anthony Stirling
e7356a1d38 Merge branch 'main' into bug/remember-me 2024-11-07 20:39:19 +00:00
a
1405e4f5ee Merge branch 'bug/remember-me' of git@github.com:Stirling-Tools/Stirling-PDF.git into bug/remember-me 2024-11-07 20:38:54 +00:00
Anthony Stirling
322e7dee0d exe no longer disable CLI 2024-11-07 20:38:32 +00:00
github-actions[bot]
918e977c6a Update translation files (#2185)
Signed-off-by: GitHub Action <action@github.com>
Co-authored-by: GitHub Action <action@github.com>
2024-11-05 14:26:24 +00:00
Anthony Stirling
a31633e5b8 Merge branch 'main' into bug/remember-me 2024-11-05 14:24:44 +00:00
Anthony Stirling
ed551cec91 remove uselss comment 2024-11-05 14:24:28 +00:00
Anthony Stirling
3f14e77725 fix remmeber me 2024-11-05 14:22:55 +00:00
66 changed files with 460 additions and 636 deletions

View File

@@ -1,179 +0,0 @@
name: PR Deployment via Comment
on:
issue_comment:
types: [created]
jobs:
check-comment:
runs-on: ubuntu-latest
if: |
github.event.issue.pull_request &&
(
contains(github.event.comment.body, 'prdeploy') ||
contains(github.event.comment.body, 'deploypr')
)
&&
(
github.event.comment.user.login == 'frooodle' ||
github.event.comment.user.login == 'sf298' ||
github.event.comment.user.login == 'Ludy87' ||
github.event.comment.user.login == 'LaserKaspar' ||
github.event.comment.user.login == 'sbplat' ||
github.event.comment.user.login == 'reecebrowne'
)
outputs:
pr_number: ${{ steps.get-pr.outputs.pr_number }}
pr_repository: ${{ steps.get-pr-info.outputs.repository }}
pr_ref: ${{ steps.get-pr-info.outputs.ref }}
steps:
- name: Get PR data
id: get-pr
uses: actions/github-script@v7
with:
script: |
const prNumber = context.payload.issue.number;
console.log(`PR Number: ${prNumber}`);
core.setOutput('pr_number', prNumber);
- name: Get PR repository and ref
id: get-pr-info
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.issue.number;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber,
});
// For forks, use the full repository name, for internal PRs use the current repo
const repository = pr.head.repo.fork ? pr.head.repo.full_name : `${owner}/${repo}`;
console.log(`PR Repository: ${repository}`);
console.log(`PR Branch: ${pr.head.ref}`);
core.setOutput('repository', repository);
core.setOutput('ref', pr.head.ref);
deploy-pr:
needs: check-comment
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
repository: ${{ needs.check-comment.outputs.pr_repository }}
ref: ${{ needs.check-comment.outputs.pr_ref }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Run Gradle Command
run: ./gradlew clean build
env:
DOCKER_ENABLE_SECURITY: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Build and push PR-specific image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Deploy to VPS
run: |
# First create the docker-compose content locally
cat > docker-compose.yml << 'EOF'
version: '3.3'
services:
stirling-pdf:
container_name: stirling-pdf-pr-${{ needs.check-comment.outputs.pr_number }}
image: ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ needs.check-comment.outputs.pr_number }}
ports:
- "${{ needs.check-comment.outputs.pr_number }}:8080"
volumes:
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/data:/usr/share/tessdata:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/config:/configs:rw
- /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-GB
UI_APPNAME: "Stirling-PDF PR#${{ needs.check-comment.outputs.pr_number }}"
UI_HOMEDESCRIPTION: "PR#${{ needs.check-comment.outputs.pr_number }} for Stirling-PDF Latest"
UI_APPNAMENAVBAR: "PR#${{ needs.check-comment.outputs.pr_number }}"
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "false"
restart: on-failure:5
EOF
# Then copy the file and execute commands
scp -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null docker-compose.yml ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }}:/tmp/docker-compose.yml
ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
# Create PR-specific directories
mkdir -p /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/{data,config,logs}
# Move docker-compose file to correct location
mv /tmp/docker-compose.yml /stirling/PR-${{ needs.check-comment.outputs.pr_number }}/docker-compose.yml
# Start or restart the container
cd /stirling/PR-${{ needs.check-comment.outputs.pr_number }}
docker-compose pull
docker-compose up -d
ENDSSH
- name: Post deployment URL to PR
if: success()
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = ${{ needs.check-comment.outputs.pr_number }};
const deploymentUrl = `http://${{ secrets.VPS_HOST }}:${prNumber}`;
const commentBody = `## 🚀 PR Test Deployment\n\n` +
`Your PR has been deployed for testing!\n\n` +
`🔗 **Test URL:** [${deploymentUrl}](${deploymentUrl})\n\n` +
`This deployment will be automatically cleaned up when the PR is closed.\n\n`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: commentBody
});

View File

@@ -1,78 +0,0 @@
name: PR Deployment cleanup
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
contents: write
pull-requests: write
env:
SERVER_IP: ${{ secrets.VPS_IP }} # Add this to your GitHub secrets
CLEANUP_PERFORMED: 'false' # Add flag to track if cleanup occurred
jobs:
cleanup:
runs-on: ubuntu-latest
if: github.event.action == 'closed'
steps:
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.VPS_SSH_KEY }}" > ../private.key
sudo chmod 600 ../private.key
- name: Cleanup PR deployment
id: cleanup
run: |
CLEANUP_STATUS=$(ssh -i ../private.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -T ${{ secrets.VPS_USERNAME }}@${{ secrets.VPS_HOST }} << 'ENDSSH'
if [ -d "/stirling/PR-${{ github.event.pull_request.number }}" ]; then
echo "Found PR directory, proceeding with cleanup..."
# Stop and remove containers
cd /stirling/PR-${{ github.event.pull_request.number }}
docker-compose down || true
# Go back to root before removal
cd /
# Remove PR-specific directories
rm -rf /stirling/PR-${{ github.event.pull_request.number }}
# Remove the Docker image
docker rmi --no-prune ${{ secrets.DOCKER_HUB_USERNAME }}/test:pr-${{ github.event.pull_request.number }} || true
echo "PERFORMED_CLEANUP"
else
echo "PR directory not found, nothing to clean up"
echo "NO_CLEANUP_NEEDED"
fi
ENDSSH
)
if [[ $CLEANUP_STATUS == *"PERFORMED_CLEANUP"* ]]; then
echo "cleanup_performed=true" >> $GITHUB_OUTPUT
else
echo "cleanup_performed=false" >> $GITHUB_OUTPUT
fi
- name: Post cleanup notice to PR
if: steps.cleanup.outputs.cleanup_performed == 'true'
uses: actions/github-script@v7
with:
script: |
const { GITHUB_REPOSITORY } = process.env;
const [repoOwner, repoName] = GITHUB_REPOSITORY.split('/');
const prNumber = context.issue.number;
const commentBody = `## 🧹 Deployment Cleanup\n\n` +
`The test deployment for this PR has been cleaned up.`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: commentBody
});

View File

@@ -9,7 +9,6 @@ on:
paths: paths:
- "src/main/resources/messages_en_GB.properties" - "src/main/resources/messages_en_GB.properties"
# Permissions required for the workflow
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
@@ -19,13 +18,6 @@ jobs:
if: github.event_name == 'pull_request_target' if: github.event_name == 'pull_request_target'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout main branch first
uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Checkout PR branch - name: Checkout PR branch
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -34,6 +26,13 @@ jobs:
path: pr-branch path: pr-branch
fetch-depth: 0 fetch-depth: 0
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: main
path: main-branch
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
@@ -50,46 +49,34 @@ jobs:
echo "Fetching PR changed files..." echo "Fetching PR changed files..."
cd pr-branch cd pr-branch
gh repo set-default ${{ github.repository }} gh repo set-default ${{ github.repository }}
# Store files in a safe way, only allowing valid properties files gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" > ../changed_files.txt
echo "Getting list of changed files from PR..."
gh pr view ${{ github.event.pull_request.number }} --json files -q ".files[].path" | grep -E '^src/main/resources/messages_[a-zA-Z_]+\.properties$' > ../changed_files.txt
cd .. cd ..
echo "Setting branch path..." echo $(cat changed_files.txt)
BRANCH_PATH="pr-branch" BRANCH_PATH="pr-branch"
echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV echo "BRANCH_PATH=${BRANCH_PATH}" >> $GITHUB_ENV
echo "Processing changed files..." CHANGED_FILES=$(cat changed_files.txt | tr '\n' ' ')
mapfile -t CHANGED_FILES < changed_files.txt echo "CHANGED_FILES=${CHANGED_FILES}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES}"
CHANGED_FILES_STR="${CHANGED_FILES[*]}"
echo "CHANGED_FILES=${CHANGED_FILES_STR}" >> $GITHUB_ENV
echo "Changed files: ${CHANGED_FILES_STR}"
echo "Branch: ${BRANCH_PATH}" echo "Branch: ${BRANCH_PATH}"
- name: Determine reference file - name: Determine reference file
id: determine-file id: determine-file
run: | run: |
echo "Determining reference file..." echo "Determining reference file..."
if grep -Fxq "src/main/resources/messages_en_GB.properties" changed_files.txt; then if echo "${{ env.CHANGED_FILES }}" | grep -q 'src/main/resources/messages_en_GB.properties'; then
echo "Using PR branch reference file"
echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV echo "REFERENCE_FILE=pr-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
else else
echo "Using main branch reference file"
echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV echo "REFERENCE_FILE=main-branch/src/main/resources/messages_en_GB.properties" >> $GITHUB_ENV
fi fi
echo "REFERENCE_FILE=${{ env.REFERENCE_FILE }}"
- name: Show REFERENCE_FILE - name: Show REFERENCE_FILE
run: echo "Reference file is set to ${REFERENCE_FILE}" run: echo "Reference file is set to ${{ env.REFERENCE_FILE }}"
- name: Run Python script to check files - name: Run Python script to check files
id: run-check id: run-check
run: | run: |
echo "Running Python script to check files..." python main-branch/.github/scripts/check_language_properties.py --reference-file ${{ env.REFERENCE_FILE }} --branch ${{ env.BRANCH_PATH }} --files ${{ env.CHANGED_FILES }} > failure.txt || true
python main-branch/.github/scripts/check_language_properties.py \
--reference-file "${REFERENCE_FILE}" \
--branch "${BRANCH_PATH}" \
--files ${CHANGED_FILES} > failure.txt || true
- name: Capture output - name: Capture output
id: capture-output id: capture-output
@@ -100,7 +87,7 @@ jobs:
echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV echo "ERROR_OUTPUT<<EOF" >> $GITHUB_ENV
echo "$ERROR_OUTPUT" >> $GITHUB_ENV echo "$ERROR_OUTPUT" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV
echo "${ERROR_OUTPUT}" echo $ERROR_OUTPUT
else else
echo "No errors found." echo "No errors found."
echo "ERROR_OUTPUT=" >> $GITHUB_ENV echo "ERROR_OUTPUT=" >> $GITHUB_ENV
@@ -182,10 +169,7 @@ jobs:
- name: Run Python script to check files - name: Run Python script to check files
id: run-check id: run-check
run: | run: |
echo "Running Python script to check files..." python .github/scripts/check_language_properties.py --reference-file src/main/resources/messages_en_GB.properties --branch main
python .github/scripts/check_language_properties.py \
--reference-file src/main/resources/messages_en_GB.properties \
--branch main
- name: Set up git config - name: Set up git config
run: | run: |

View File

@@ -67,7 +67,6 @@ jobs:
images: | images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: | tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
@@ -96,7 +95,6 @@ jobs:
images: | images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: | tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
@@ -124,7 +122,6 @@ jobs:
images: | images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/stirling-pdf
${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf ${{ secrets.DOCKER_HUB_ORG_USERNAME }}/stirling-pdf
tags: | tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-fat,enable=${{ github.ref == 'refs/heads/master' }}

View File

@@ -114,7 +114,7 @@ These files provide pre-configured setups for different scenarios. For example,
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security container_name: Stirling-PDF-Security
image: stirlingtools/stirling-pdf:latest image: frooodle/s-pdf:latest
deploy: deploy:
resources: resources:
limits: limits:
@@ -173,20 +173,20 @@ Stirling-PDF uses different Docker images for various configurations. The build
For the latest version: For the latest version:
```bash ```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest -f ./Dockerfile . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest -f ./Dockerfile .
``` ```
For the ultra-lite version: For the ultra-lite version:
```bash ```bash
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-ultra-lite -f ./Dockerfile-ultra-lite .
``` ```
For the fat version (with security enabled): For the fat version (with security enabled):
```bash ```bash
export DOCKER_ENABLE_SECURITY=true export DOCKER_ENABLE_SECURITY=true
docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t stirlingtools/stirling-pdf:latest-fat -f ./Dockerfile-fat . docker build --no-cache --pull --build-arg VERSION_TAG=alpha -t frooodle/s-pdf:latest-fat -f ./Dockerfile-fat .
``` ```
Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase Note: The `--no-cache` and `--pull` flags ensure that the build process uses the latest base images and doesn't use cached layers, which is useful for testing and ensuring reproducible builds. however to improve build times these can often be removed depending on your usecase

45
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,45 @@
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'chmod 755 gradlew'
sh './gradlew build'
}
}
stage('Docker Build') {
steps {
script {
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
def image = "frooodle/s-pdf:$appVersion"
sh "docker build -t $image ."
}
}
}
stage('Docker Push') {
steps {
script {
def appVersion = sh(returnStdout: true, script: './gradlew printVersion -q').trim()
def image = "frooodle/s-pdf:$appVersion"
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
sh "docker push $image"
}
}
}
}
stage('Helm Push') {
steps {
script {
//TODO: Read chartVersion from Chart.yaml
def chartVersion = '1.0.0'
withCredentials([string(credentialsId: 'docker_hub_access_token', variable: 'DOCKER_HUB_ACCESS_TOKEN')]) {
sh "docker login --username frooodle --password $DOCKER_HUB_ACCESS_TOKEN"
sh "helm package chart/stirling-pdf"
sh "helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle"
}
}
}
}
}
}

View File

@@ -120,13 +120,13 @@ Please view the [LocalRunGuide](https://github.com/Stirling-Tools/Stirling-PDF/b
### Docker / Podman ### Docker / Podman
> [!NOTE] > [!NOTE]
> <https://hub.docker.com/r/stirlingtools/stirling-pdf> > <https://hub.docker.com/r/frooodle/s-pdf>
Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag. Stirling-PDF has three different versions: a full version, an ultra-lite version, and a 'fat' version. Depending on the types of features you use, you may want a smaller image to save on space. To see what the different versions offer, please look at our [version mapping](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Version-groups.md). For people that don't mind space optimization, just use the latest tag.
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest?label=Stirling-PDF%20Full) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/stirlingtools/stirling-pdf/latest-fat?label=Stirling-PDF%20Fat) ![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-fat?label=Stirling-PDF%20Fat)
Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`. Please note in the examples below, you may need to change the volume paths as needed, e.g., `./extraConfigs:/configs` to `/opt/stirlingpdf/extraConfigs:/configs`.
@@ -144,7 +144,7 @@ docker run -d \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \ -e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \ -e LANGS=en_GB \
--name stirling-pdf \ --name stirling-pdf \
stirlingtools/stirling-pdf:latest frooodle/s-pdf:latest
``` ```
### Docker Compose ### Docker Compose
@@ -153,7 +153,7 @@ docker run -d \
version: '3.3' version: '3.3'
services: services:
stirling-pdf: stirling-pdf:
image: stirlingtools/stirling-pdf:latest image: frooodle/s-pdf:latest
ports: ports:
- '8080:8080' - '8080:8080'
volumes: volumes:
@@ -190,29 +190,29 @@ Stirling-PDF currently supports 36 languages!
| Language | Progress | | Language | Progress |
| -------------------------------------------- | -------------------------------------- | | -------------------------------------------- | -------------------------------------- |
| Arabic (العربية) (ar_AR) | ![97%](https://geps.dev/progress/97) | | Arabic (العربية) (ar_AR) | ![98%](https://geps.dev/progress/98) |
| Basque (Euskara) (eu_ES) | ![55%](https://geps.dev/progress/55) | | Basque (Euskara) (eu_ES) | ![55%](https://geps.dev/progress/55) |
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) | | Bulgarian (Български) (bg_BG) | ![97%](https://geps.dev/progress/97) |
| Catalan (Català) (ca_CA) | ![90%](https://geps.dev/progress/90) | | Catalan (Català) (ca_CA) | ![90%](https://geps.dev/progress/90) |
| Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) | | Croatian (Hrvatski) (hr_HR) | ![98%](https://geps.dev/progress/98) |
| Czech (Česky) (cs_CZ) | ![97%](https://geps.dev/progress/97) | | Czech (Česky) (cs_CZ) | ![98%](https://geps.dev/progress/98) |
| Danish (Dansk) (da_DK) | ![96%](https://geps.dev/progress/96) | | Danish (Dansk) (da_DK) | ![97%](https://geps.dev/progress/97) |
| Dutch (Nederlands) (nl_NL) | ![96%](https://geps.dev/progress/96) | | Dutch (Nederlands) (nl_NL) | ![96%](https://geps.dev/progress/96) |
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | | English (English) (en_GB) | ![100%](https://geps.dev/progress/100) |
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) | | English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) | | French (Français) (fr_FR) | ![97%](https://geps.dev/progress/97) |
| German (Deutsch) (de_DE) | ![99%](https://geps.dev/progress/99) | | German (Deutsch) (de_DE) | ![98%](https://geps.dev/progress/98) |
| Greek (Ελληνικά) (el_GR) | ![97%](https://geps.dev/progress/97) | | Greek (Ελληνικά) (el_GR) | ![98%](https://geps.dev/progress/98) |
| Hindi (हिंदी) (hi_IN) | ![95%](https://geps.dev/progress/95) | | Hindi (हिंदी) (hi_IN) | ![95%](https://geps.dev/progress/95) |
| Hungarian (Magyar) (hu_HU) | ![98%](https://geps.dev/progress/98) | | Hungarian (Magyar) (hu_HU) | ![98%](https://geps.dev/progress/98) |
| Indonesian (Bahasa Indonesia) (id_ID) | ![97%](https://geps.dev/progress/97) | | Indonesian (Bahasa Indonesia) (id_ID) | ![98%](https://geps.dev/progress/98) |
| Irish (Gaeilge) (ga_IE) | ![88%](https://geps.dev/progress/88) | | Irish (Gaeilge) (ga_IE) | ![88%](https://geps.dev/progress/88) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) | | Italian (Italiano) (it_IT) | ![98%](https://geps.dev/progress/98) |
| Japanese (日本語) (ja_JP) | ![85%](https://geps.dev/progress/85) | | Japanese (日本語) (ja_JP) | ![86%](https://geps.dev/progress/86) |
| Korean (한국어) (ko_KR) | ![95%](https://geps.dev/progress/95) | | Korean (한국어) (ko_KR) | ![96%](https://geps.dev/progress/96) |
| Norwegian (Norsk) (no_NB) | ![87%](https://geps.dev/progress/87) | | Norwegian (Norsk) (no_NB) | ![88%](https://geps.dev/progress/88) |
| Polish (Polski) (pl_PL) | ![97%](https://geps.dev/progress/97) | | Polish (Polski) (pl_PL) | ![97%](https://geps.dev/progress/97) |
| Portuguese (Português) (pt_PT) | ![97%](https://geps.dev/progress/97) | | Portuguese (Português) (pt_PT) | ![98%](https://geps.dev/progress/98) |
| Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) | | Portuguese Brazilian (Português) (pt_BR) | ![98%](https://geps.dev/progress/98) |
| Romanian (Română) (ro_RO) | ![90%](https://geps.dev/progress/90) | | Romanian (Română) (ro_RO) | ![90%](https://geps.dev/progress/90) |
| Russian (Русский) (ru_RU) | ![97%](https://geps.dev/progress/97) | | Russian (Русский) (ru_RU) | ![97%](https://geps.dev/progress/97) |
@@ -225,7 +225,7 @@ Stirling-PDF currently supports 36 languages!
| Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) | | Traditional Chinese (繁體中文) (zh_TW) | ![98%](https://geps.dev/progress/98) |
| Turkish (Türkçe) (tr_TR) | ![92%](https://geps.dev/progress/92) | | Turkish (Türkçe) (tr_TR) | ![92%](https://geps.dev/progress/92) |
| Ukrainian (Українська) (uk_UA) | ![80%](https://geps.dev/progress/80) | | Ukrainian (Українська) (uk_UA) | ![80%](https://geps.dev/progress/80) |
| Vietnamese (Tiếng Việt) (vi_VN) | ![88%](https://geps.dev/progress/88) | | Vietnamese (Tiếng Việt) (vi_VN) | ![89%](https://geps.dev/progress/89) |
## Contributing (Creating Issues, Translations, Fixing Bugs, etc.) ## Contributing (Creating Issues, Translations, Fixing Bugs, etc.)

View File

@@ -10,8 +10,6 @@ plugins {
//id "nebula.lint" version "19.0.3" //id "nebula.lint" version "19.0.3"
} }
import com.github.jk1.license.render.* import com.github.jk1.license.render.*
ext { ext {
@@ -19,12 +17,12 @@ ext {
pdfboxVersion = "3.0.3" pdfboxVersion = "3.0.3"
logbackVersion = "1.5.7" logbackVersion = "1.5.7"
imageioVersion = "3.12.0" imageioVersion = "3.12.0"
lombokVersion = "1.18.36" lombokVersion = "1.18.34"
bouncycastleVersion = "1.78.1" bouncycastleVersion = "1.78.1"
} }
group = "stirling.software" group = "stirling.software"
version = "0.33.1" version = "0.32.0"
java { java {
// 17 is lowest but we support and recommend 21 // 17 is lowest but we support and recommend 21

View File

@@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security-Fat container_name: Stirling-PDF-Security-Fat
image: stirlingtools/stirling-pdf:latest-fat image: frooodle/s-pdf:latest-fat
deploy: deploy:
resources: resources:
limits: limits:

View File

@@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security container_name: Stirling-PDF-Security
image: stirlingtools/stirling-pdf:latest image: frooodle/s-pdf:latest
deploy: deploy:
resources: resources:
limits: limits:

View File

@@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF-Security container_name: Stirling-PDF-Security
image: stirlingtools/stirling-pdf:latest image: frooodle/s-pdf:latest
deploy: deploy:
resources: resources:
limits: limits:

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
services: services:
stirling-pdf: stirling-pdf:
container_name: Stirling-PDF container_name: Stirling-PDF
image: stirlingtools/stirling-pdf:latest image: frooodle/s-pdf:latest
deploy: deploy:
resources: resources:
limits: limits:

View File

@@ -32,12 +32,12 @@ ignore = [
ignore = [ ignore = [
'AddStampRequest.alphabet', 'AddStampRequest.alphabet',
'AddStampRequest.position', 'AddStampRequest.position',
'home.pipeline.title'
'PDFToBook.selectText.1', 'PDFToBook.selectText.1',
'PDFToText.tags', 'PDFToText.tags',
'addPageNumbers.selectText.3', 'addPageNumbers.selectText.3',
'alphabet', 'alphabet',
'certSign.name', 'certSign.name',
'home.pipeline.title',
'language.direction', 'language.direction',
'licenses.version', 'licenses.version',
'pipeline.title', 'pipeline.title',
@@ -46,6 +46,7 @@ ignore = [
'sponsor', 'sponsor',
'text', 'text',
'watermark.type.1', 'watermark.type.1',
'certSign.name',
] ]
[el_GR] [el_GR]

View File

@@ -34,12 +34,6 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
@Value("${spring.datasource.url}") @Value("${spring.datasource.url}")
private String url; private String url;
@Value("${spring.datasource.username}")
private String databaseUsername;
@Value("${spring.datasource.password}")
private String databasePassword;
private Path backupPath = Paths.get("configs/db/backup/"); private Path backupPath = Paths.get("configs/db/backup/");
@Override @Override
@@ -140,8 +134,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql"); this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;"; String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
try (Connection conn = try (Connection conn = DriverManager.getConnection(url, "sa", "");
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) { PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, insertOutputFilePath.toString()); stmt.setString(1, insertOutputFilePath.toString());
stmt.execute(); stmt.execute();
@@ -154,8 +147,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
// Retrieves the H2 database version. // Retrieves the H2 database version.
public String getH2Version() { public String getH2Version() {
String version = "Unknown"; String version = "Unknown";
try (Connection conn = try (Connection conn = DriverManager.getConnection(url, "sa", "")) {
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
try (Statement stmt = conn.createStatement(); try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) { ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
if (rs.next()) { if (rs.next()) {
@@ -197,8 +189,7 @@ public class DatabaseBackupHelper implements DatabaseBackupInterface {
private boolean executeDatabaseScript(Path scriptPath) { private boolean executeDatabaseScript(Path scriptPath) {
String query = "RUNSCRIPT from ?;"; String query = "RUNSCRIPT from ?;";
try (Connection conn = try (Connection conn = DriverManager.getConnection(url, "sa", "");
DriverManager.getConnection(url, databaseUsername, databasePassword);
PreparedStatement stmt = conn.prepareStatement(query)) { PreparedStatement stmt = conn.prepareStatement(query)) {
stmt.setString(1, scriptPath.toString()); stmt.setString(1, scriptPath.toString());
stmt.execute(); stmt.execute();

View File

@@ -1,18 +1,25 @@
package stirling.software.SPDF.config.security.saml2; package stirling.software.SPDF.config.security.saml2;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.KeyFactory; import java.security.KeyFactory;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Scanner;
import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader; import org.bouncycastle.util.io.pem.PemReader;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CertificateUtils { public class CertificateUtils {
public static X509Certificate readCertificate(Resource certificateResource) throws Exception { public static X509Certificate readCertificate(Resource certificateResource) throws Exception {
@@ -39,4 +46,84 @@ public class CertificateUtils {
.generatePrivate(new PKCS8EncodedKeySpec(decodedKey)); .generatePrivate(new PKCS8EncodedKeySpec(decodedKey));
} }
} }
public static X509Certificate getIdPCertificate(Resource certificateResource) throws Exception {
if (certificateResource instanceof UrlResource) {
return extractCertificateFromMetadata(certificateResource);
} else {
// Treat as file resource
return readCertificate(certificateResource);
}
}
private static X509Certificate extractCertificateFromMetadata(Resource metadataResource) throws Exception {
log.info("Attempting to extract certificate from metadata resource: {}", metadataResource.getDescription());
try (InputStream is = metadataResource.getInputStream()) {
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
log.info("Retrieved metadata content, length: {}", content.length());
// Find the certificate data
int startIndex = content.indexOf("<ds:X509Certificate>");
int endIndex = content.indexOf("</ds:X509Certificate>");
if (startIndex == -1 || endIndex == -1) {
log.error("Certificate tags not found in metadata");
throw new Exception("Certificate tags not found in metadata");
}
// Extract certificate data
String certData = content.substring(
startIndex + "<ds:X509Certificate>".length(),
endIndex
).trim();
log.info("Found certificate data, length: {}", certData.length());
// Remove any whitespace and newlines from cert data
certData = certData.replaceAll("\\s+", "");
// Reconstruct PEM format with proper line breaks
StringBuilder pemBuilder = new StringBuilder();
pemBuilder.append("-----BEGIN CERTIFICATE-----\n");
// Insert line breaks every 64 characters
int lineLength = 64;
for (int i = 0; i < certData.length(); i += lineLength) {
int end = Math.min(i + lineLength, certData.length());
pemBuilder.append(certData, i, end).append('\n');
}
pemBuilder.append("-----END CERTIFICATE-----");
String pemCert = pemBuilder.toString();
log.debug("Reconstructed PEM certificate:\n{}", pemCert);
try {
ByteArrayInputStream pemStream = new ByteArrayInputStream(pemCert.getBytes(StandardCharsets.UTF_8));
CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(pemStream);
log.info("Successfully parsed certificate. Subject: {}", cert.getSubjectX500Principal());
// Optional: check validity dates
cert.checkValidity(); // Throws CertificateExpiredException if expired
log.info("Certificate is valid (not expired)");
return cert;
} catch (Exception e) {
log.error("Failed to parse certificate", e);
throw new Exception("Failed to parse X509 certificate from metadata", e);
}
} catch (Exception e) {
log.error("Error processing metadata resource", e);
throw e;
}
}
} }

View File

@@ -16,23 +16,35 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public class CustomSaml2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Override @Override
public void onAuthenticationFailure( public void onAuthenticationFailure(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
AuthenticationException exception) AuthenticationException exception)
throws IOException, ServletException { throws IOException, ServletException {
if (exception instanceof Saml2AuthenticationException) {
Saml2Error error = ((Saml2AuthenticationException) exception).getSaml2Error(); if (exception instanceof Saml2AuthenticationException saml2Exception) {
getRedirectStrategy() Saml2Error error = saml2Exception.getSaml2Error();
.sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
} else if (exception instanceof ProviderNotFoundException) { // Log detailed information about the SAML error
getRedirectStrategy() log.error("SAML Authentication failed with error code: {}", error.getErrorCode());
.sendRedirect( log.error("Error description: {}", error.getDescription());
request,
response, // Redirect to login with specific error code
"/login?erroroauth=not_authentication_provider_found"); getRedirectStrategy()
} .sendRedirect(request, response, "/login?erroroauth=" + error.getErrorCode());
log.error("AuthenticationException: " + exception); } else if (exception instanceof ProviderNotFoundException) {
} log.error("Authentication failed: No authentication provider found");
getRedirectStrategy()
.sendRedirect(
request,
response,
"/login?erroroauth=not_authentication_provider_found");
} else {
log.error("Unknown AuthenticationException: {}", exception.getMessage());
getRedirectStrategy().sendRedirect(request, response, "/login?erroroauth=unknown_error");
}
}
} }

View File

@@ -0,0 +1,67 @@
package stirling.software.SPDF.config.security.saml2;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationToken;
import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml2.core.Saml2ErrorCodes;
public class LoggingSamlAuthenticationProvider implements AuthenticationProvider {
private static final Logger log = LoggerFactory.getLogger(LoggingSamlAuthenticationProvider.class);
private final OpenSaml4AuthenticationProvider delegate;
public LoggingSamlAuthenticationProvider(OpenSaml4AuthenticationProvider delegate) {
this.delegate = delegate;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication instanceof Saml2AuthenticationToken token) {
String samlResponse = token.getSaml2Response();
// Log the raw SAML response
log.info("Raw SAML Response (Base64): {}", samlResponse);
// Decode and log the SAML response XML
try {
String decodedResponse = new String(Base64.getDecoder().decode(samlResponse), StandardCharsets.UTF_8);
log.info("Decoded SAML Response XML:\n{}", decodedResponse);
} catch (IllegalArgumentException e) {
// If decoding fails, its likely already plain XML
log.warn("SAML Response appears to be different format, not Base64-encoded.");
log.debug("SAML Response XML:\n{}", samlResponse);
}
// Delegate the actual authentication to the wrapped OpenSaml4AuthenticationProvider
try {
return delegate.authenticate(authentication);
} catch (Saml2AuthenticationException e) {
log.error("SAML authentication failed: {}");
log.error("Detailed error message: {}", e);
throw e;
}
}
return null;
}
@Override
public boolean supports(Class<?> authentication) {
// Only support Saml2AuthenticationToken
return Saml2AuthenticationToken.class.isAssignableFrom(authentication);
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import lombok.Data; import lombok.Data;
import lombok.Getter; import lombok.Getter;
@@ -30,6 +31,7 @@ import stirling.software.SPDF.model.provider.GithubProvider;
import stirling.software.SPDF.model.provider.GoogleProvider; import stirling.software.SPDF.model.provider.GoogleProvider;
import stirling.software.SPDF.model.provider.KeycloakProvider; import stirling.software.SPDF.model.provider.KeycloakProvider;
import stirling.software.SPDF.model.provider.UnsupportedProviderException; import stirling.software.SPDF.model.provider.UnsupportedProviderException;
import stirling.software.SPDF.utils.GeneralUtils;
@Configuration @Configuration
@ConfigurationProperties(prefix = "") @ConfigurationProperties(prefix = "")
@@ -134,44 +136,20 @@ public class ApplicationProperties {
private String privateKey; private String privateKey;
private String spCert; private String spCert;
public InputStream getIdpMetadataUri() throws IOException { public Resource getIdpMetadataUri() throws IOException {
if (idpMetadataUri.startsWith("classpath:")) { return GeneralUtils.filePathToResource(idpMetadataUri);
return new ClassPathResource(idpMetadataUri.substring("classpath".length()))
.getInputStream();
}
try {
URI uri = new URI(idpMetadataUri);
URL url = uri.toURL();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
return connection.getInputStream();
} catch (URISyntaxException e) {
throw new IOException("Invalid URI format: " + idpMetadataUri, e);
}
} }
public Resource getSpCert() { public Resource getSpCert() {
if (spCert.startsWith("classpath:")) { return GeneralUtils.filePathToResource(spCert);
return new ClassPathResource(spCert.substring("classpath:".length()));
} else {
return new FileSystemResource(spCert);
}
} }
public Resource getidpCert() { public Resource getidpCert() {
if (idpCert.startsWith("classpath:")) { return GeneralUtils.filePathToResource(idpCert);
return new ClassPathResource(idpCert.substring("classpath:".length()));
} else {
return new FileSystemResource(idpCert);
}
} }
public Resource getPrivateKey() { public Resource getPrivateKey() {
if (privateKey.startsWith("classpath:")) { return GeneralUtils.filePathToResource(privateKey);
return new ClassPathResource(privateKey.substring("classpath:".length()));
} else {
return new FileSystemResource(privateKey);
}
} }
} }

View File

@@ -32,19 +32,11 @@ public class MetricsAggregatorService {
.counters() .counters()
.forEach( .forEach(
counter -> { counter -> {
String method = counter.getId().getTag("method"); String key =
String uri = counter.getId().getTag("uri"); String.format(
"http_requests_%s_%s",
// Skip if either method or uri is null counter.getId().getTag("method"),
if (method == null || uri == null) { counter.getId().getTag("uri").replace("/", "_"));
return;
}
String key = String.format(
"http_requests_%s_%s",
method,
uri.replace("/", "_")
);
double currentCount = counter.count(); double currentCount = counter.count();
double lastCount = lastSentMetrics.getOrDefault(key, 0.0); double lastCount = lastSentMetrics.getOrDefault(key, 0.0);

View File

@@ -29,6 +29,10 @@ import org.simpleyaml.configuration.implementation.SimpleYamlImplementation;
import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions; import org.simpleyaml.configuration.implementation.snakeyaml.lib.DumperOptions;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import com.fathzer.soft.javaluator.DoubleEvaluator; import com.fathzer.soft.javaluator.DoubleEvaluator;
@@ -349,4 +353,23 @@ public class GeneralUtils {
return "GenericID"; return "GenericID";
} }
} }
public static Resource filePathToResource(String resourceFile) {
if (resourceFile == null) {
throw new IllegalStateException("file is not configured");
}
if (resourceFile.startsWith("classpath:")) {
return new ClassPathResource(resourceFile.substring("classpath:".length()));
} else if (resourceFile.startsWith("http://") || resourceFile.startsWith("https://")) {
try {
return new UrlResource(resourceFile);
} catch (Exception e) {
throw new RuntimeException("Failed to create URL resource: " + resourceFile, e);
}
} else {
return new FileSystemResource(resourceFile);
}
}
} }

View File

@@ -81,7 +81,6 @@ page=صفحة
pages=صفحات pages=صفحات
loading=جارٍ التحميل... loading=جارٍ التحميل...
addToDoc=إضافة إلى المستند addToDoc=إضافة إلى المستند
reset=Reset
legal.privacy=سياسة الخصوصية legal.privacy=سياسة الخصوصية
legal.terms=شروط الاستخدام legal.terms=شروط الاستخدام

View File

@@ -81,7 +81,6 @@ page=Страница
pages=Страници pages=Страници
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Политика за поверителност legal.privacy=Политика за поверителност
legal.terms=Правила и условия legal.terms=Правила и условия

View File

@@ -81,7 +81,6 @@ page=Pàgina
pages=Pàgines pages=Pàgines
loading=Carregant... loading=Carregant...
addToDoc=Afegeix al document addToDoc=Afegeix al document
reset=Reset
legal.privacy=Política de Privacitat legal.privacy=Política de Privacitat
legal.terms=Termes i condicions legal.terms=Termes i condicions

View File

@@ -81,7 +81,6 @@ page=Strana
pages=Strany pages=Strany
loading=Načítání... loading=Načítání...
addToDoc=Přidat do dokumentu addToDoc=Přidat do dokumentu
reset=Reset
legal.privacy=Politika soukromí legal.privacy=Politika soukromí
legal.terms=Podmínky použití legal.terms=Podmínky použití

View File

@@ -81,7 +81,6 @@ page=Sidenummer
pages=Sideantal pages=Sideantal
loading=Laster... loading=Laster...
addToDoc=Tilføj til Dokument addToDoc=Tilføj til Dokument
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Vilkår og betingelser legal.terms=Vilkår og betingelser

View File

@@ -81,7 +81,6 @@ page=Seite
pages=Seiten pages=Seiten
loading=Laden... loading=Laden...
addToDoc=In Dokument hinzufügen addToDoc=In Dokument hinzufügen
reset=Reset
legal.privacy=Datenschutz legal.privacy=Datenschutz
legal.terms=AGB legal.terms=AGB

View File

@@ -81,7 +81,6 @@ page=Σελίδα
pages=Σελίδες pages=Σελίδες
loading=Φόρτωση... loading=Φόρτωση...
addToDoc=Πρόσθεση στο Εκπομπώματο addToDoc=Πρόσθεση στο Εκπομπώματο
reset=Reset
legal.privacy=Πολιτική Προνομίους legal.privacy=Πολιτική Προνομίους
legal.terms=Φράσεις Υποχρεωτικότητας legal.terms=Φράσεις Υποχρεωτικότητας

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas pages=Páginas
loading=Cargando... loading=Cargando...
addToDoc=Agregar al Documento addToDoc=Agregar al Documento
reset=Reset
legal.privacy=Política de Privacidad legal.privacy=Política de Privacidad
legal.terms=Términos y Condiciones legal.terms=Términos y Condiciones

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Chargement... loading=Chargement...
addToDoc=Ajouter au Document addToDoc=Ajouter au Document
reset=Reset
legal.privacy=Politique de Confidentialité legal.privacy=Politique de Confidentialité
legal.terms=Conditions Générales legal.terms=Conditions Générales

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=पृष्ठ
pages=पृष्ठों pages=पृष्ठों
loading=डालिंग... loading=डालिंग...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=गुप्तता सूचना legal.privacy=गुप्तता सूचना
legal.terms=शर्तें और प्रवाह legal.terms=शर्तें और प्रवाह

View File

@@ -81,7 +81,6 @@ page=Stranica
pages=Stranice pages=Stranice
loading=Učitavanje... loading=Učitavanje...
addToDoc=Dodaj u dokument addToDoc=Dodaj u dokument
reset=Reset
legal.privacy=Politika privatnosti legal.privacy=Politika privatnosti
legal.terms=Uspe sodržine legal.terms=Uspe sodržine

View File

@@ -81,7 +81,6 @@ page=Oldal
pages=Oldalak pages=Oldalak
loading=Betöltés... loading=Betöltés...
addToDoc=Hozzáadás dokumentumba addToDoc=Hozzáadás dokumentumba
reset=Reset
legal.privacy=Adatvédelmi nyilatkozat legal.privacy=Adatvédelmi nyilatkozat
legal.terms=Feltételek és feltételek legal.terms=Feltételek és feltételek

View File

@@ -81,7 +81,6 @@ page=Halaman
pages=Halaman-halaman pages=Halaman-halaman
loading=Mengambil data... loading=Mengambil data...
addToDoc=Tambahkan ke Dokumen addToDoc=Tambahkan ke Dokumen
reset=Reset
legal.privacy=Kebijakan Privasi legal.privacy=Kebijakan Privasi
legal.terms=Syarat dan Ketentuan legal.terms=Syarat dan Ketentuan

View File

@@ -81,7 +81,6 @@ page=Pagina
pages=Pagine pages=Pagine
loading=Caricamento... loading=Caricamento...
addToDoc=Aggiungi al documento addToDoc=Aggiungi al documento
reset=Reset
legal.privacy=Informativa sulla privacy legal.privacy=Informativa sulla privacy
legal.terms=Termini e Condizioni legal.terms=Termini e Condizioni
@@ -142,7 +141,7 @@ navbar.language=Lingue
navbar.settings=Impostazioni navbar.settings=Impostazioni
navbar.allTools=Strumenti navbar.allTools=Strumenti
navbar.multiTool=Strumenti multipli navbar.multiTool=Strumenti multipli
navbar.search=Cerca navbar.search=Search
navbar.sections.organize=Organizza navbar.sections.organize=Organizza
navbar.sections.convertTo=Converti in PDF navbar.sections.convertTo=Converti in PDF
navbar.sections.convertFrom=Converti da PDF navbar.sections.convertFrom=Converti da PDF
@@ -945,7 +944,7 @@ multiTool.downloadAll=Esporta
multiTool.downloadSelected=Esporta selezionata multiTool.downloadSelected=Esporta selezionata
#multiTool-advert #multiTool-advert
multiTool-advert.message=Questa funzione è disponibile anche nella nostra <a href="{0}">pagina multi-strumento</a>. Scoprila per un'interfaccia utente pagina per pagina migliorata e funzionalità aggiuntive! multiTool-advert.message=This feature is also available in our <a href="{0}">multi-tool page</a>. Check it out for enhanced page-by-page UI and additional features!
#view pdf #view pdf
viewPdf.title=Visualizza PDF viewPdf.title=Visualizza PDF

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=プライバシーポリシー legal.privacy=プライバシーポリシー
legal.terms=利用規約 legal.terms=利用規約

View File

@@ -81,7 +81,6 @@ page=페이지
pages=페이지 pages=페이지
loading=로딩 중... loading=로딩 중...
addToDoc=문서에 추가 addToDoc=문서에 추가
reset=Reset
legal.privacy=개인 정보 정책 legal.privacy=개인 정보 정책
legal.terms=이용 약관 legal.terms=이용 약관

View File

@@ -81,7 +81,6 @@ page=Pagina
pages=Pagen pages=Pagen
loading=Laden... loading=Laden...
addToDoc=Toevoegen aan document addToDoc=Toevoegen aan document
reset=Reset
legal.privacy=Privacybeleid legal.privacy=Privacybeleid
legal.terms=Voorwaarden van gebruik legal.terms=Voorwaarden van gebruik

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Strona
pages=Strony pages=Strony
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Polityka Prywatności legal.privacy=Polityka Prywatności
legal.terms=Zasady i Postanowienia legal.terms=Zasady i Postanowienia

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas pages=Páginas
loading=Carregando... loading=Carregando...
addToDoc=Adicionar ao Documento addToDoc=Adicionar ao Documento
reset=Reset
legal.privacy=Política de Privacidade legal.privacy=Política de Privacidade
legal.terms=Termos e Condições legal.terms=Termos e Condições

View File

@@ -81,7 +81,6 @@ page=Página
pages=Páginas pages=Páginas
loading=A carregar... loading=A carregar...
addToDoc=Adicionar ao Documento addToDoc=Adicionar ao Documento
reset=Reset
legal.privacy=Política de Privacidade legal.privacy=Política de Privacidade
legal.terms=Termos e Condições legal.terms=Termos e Condições

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Страница
pages=Страницы pages=Страницы
loading=Загрузка... loading=Загрузка...
addToDoc=Добавить в документ addToDoc=Добавить в документ
reset=Reset
legal.privacy=Политика конфиденциальности legal.privacy=Политика конфиденциальности
legal.terms=Условия использования legal.terms=Условия использования

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Sidan
pages=Sidor pages=Sidor
loading=Laddar... loading=Laddar...
addToDoc=Lägg till i dokument addToDoc=Lägg till i dokument
reset=Reset
legal.privacy=Dataprotektionspolicy legal.privacy=Dataprotektionspolicy
legal.terms=Villkor och betingelser legal.terms=Villkor och betingelser

View File

@@ -81,7 +81,6 @@ page=หน้า
pages=หน้า pages=หน้า
loading=กำลังโหลด... loading=กำลังโหลด...
addToDoc=เพิ่มเข้าสู่เอกสาร addToDoc=เพิ่มเข้าสู่เอกสาร
reset=Reset
legal.privacy=นโยบายความเป็นส่วนตัว legal.privacy=นโยบายความเป็นส่วนตัว
legal.terms=ข้อกำหนดการใช้งาน legal.terms=ข้อกำหนดการใช้งาน

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Gizlilik Politikası legal.privacy=Gizlilik Politikası
legal.terms=Şartlar ve koşullar legal.terms=Şartlar ve koşullar

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=Page
pages=Pages pages=Pages
loading=Loading... loading=Loading...
addToDoc=Add to Document addToDoc=Add to Document
reset=Reset
legal.privacy=Privacy Policy legal.privacy=Privacy Policy
legal.terms=Terms and Conditions legal.terms=Terms and Conditions

View File

@@ -81,7 +81,6 @@ page=頁面
pages=頁面 pages=頁面
loading=載入中... loading=載入中...
addToDoc=新增至文件 addToDoc=新增至文件
reset=Reset
legal.privacy=隱私權政策 legal.privacy=隱私權政策
legal.terms=使用條款 legal.terms=使用條款

View File

@@ -140,7 +140,7 @@ span.icon-text::after {
position: relative; position: relative;
} }
/* .tooltip-text { .tooltip-text {
visibility: hidden; visibility: hidden;
background-color: rgb(71 67 67 / 80%); background-color: rgb(71 67 67 / 80%);
color: #fff; color: #fff;
@@ -162,7 +162,7 @@ span.icon-text::after {
.nav-item:hover .tooltip-text { .nav-item:hover .tooltip-text {
visibility: visible; visibility: visible;
opacity: 1; opacity: 1;
} */ }
.dropdown-menu.scrollable-y { .dropdown-menu.scrollable-y {
overflow-y: scroll; overflow-y: scroll;
@@ -324,18 +324,6 @@ span.icon-text::after {
margin-top: 0; margin-top: 0;
} }
/* .icon-hide {
display: none;
} */
}
@media (max-width:1199px) {
.icon-hide {
display: inline-flex;
}
}
@media (min-width:1200px) {
.icon-hide { .icon-hide {
display: none; display: none;
} }

View File

@@ -96,37 +96,71 @@
}); });
}); });
async function getPDFPageCount(file) { function getPDFPageCount(file) {
try { try {
const arrayBuffer = await file.arrayBuffer(); if (file.type !== 'application/pdf') {
pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdfjs-legacy/pdf.worker.mjs' return null;
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; }
return pdf.numPages;
// Create a URL for the file
const url = URL.createObjectURL(file);
try {
// Ensure the worker is properly set
if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
}
// Load the PDF document
const loadingTask = pdfjsLib.getDocument(url);
const pdf = await loadingTask.promise;
// Get the page count
const pageCount = pdf.numPages;
return pageCount;
} finally {
// Clean up the URL
URL.revokeObjectURL(url);
}
} catch (error) { } catch (error) {
console.error('Error getting PDF page count:', error); console.error('Error counting PDF pages:', error);
return null; return null;
} }
} }
function trackFileProcessing(file, startTime, success, errorMessage = null) {
const endTime = performance.now();
const processingTime = endTime - startTime;
posthog.capture('file_processing', {
success: success,
file_type: file.type || 'unknown',
file_size: file.size,
processing_time: processingTime,
error_message: errorMessage,
pdf_pages: file.type === 'application/pdf' ? getPDFPageCount(file) : null
});
}
async function handleSingleDownload(url, formData, isMulti = false, isZip = false) { async function handleSingleDownload(url, formData, isMulti = false, isZip = false) {
const startTime = performance.now(); const startTime = performance.now();
const file = formData.get('fileInput'); const file = formData.get('fileInput');
let success = false;
let errorMessage = null;
try { try {
const response = await fetch(url, { method: "POST", body: formData }); const response = await fetch(url, { method: "POST", body: formData });
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
if (!response.ok) { if (!response.ok) {
errorMessage = response.status;
if (response.status === 401) { if (response.status === 401) {
showSessionExpiredPrompt(); showSessionExpiredPrompt();
trackFileProcessing(file, startTime, false, 'unauthorized');
return; return;
} }
if (contentType && contentType.includes("application/json")) { if (contentType && contentType.includes("application/json")) {
console.error("Throwing error banner, response was not okay"); console.error("Throwing error banner, response was not okay");
return handleJsonResponse(response); const jsonResponse = await handleJsonResponse(response);
trackFileProcessing(file, startTime, false, jsonResponse?.error || 'unknown_error');
return jsonResponse;
} }
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
@@ -135,7 +169,7 @@
let filename = getFilenameFromContentDisposition(contentDisposition); let filename = getFilenameFromContentDisposition(contentDisposition);
const blob = await response.blob(); const blob = await response.blob();
success = true; trackFileProcessing(file, startTime, true, null);
if (contentType.includes("application/pdf") || contentType.includes("image/")) { if (contentType.includes("application/pdf") || contentType.includes("image/")) {
clearFileInput(); clearFileInput();
@@ -145,30 +179,16 @@
return handleResponse(blob, filename, false, isZip); return handleResponse(blob, filename, false, isZip);
} }
} catch (error) { } catch (error) {
success = false;
errorMessage = error.message;
clearFileInput(); clearFileInput();
console.error("Error in handleSingleDownload:", error); console.error("Error in handleSingleDownload:", error);
trackFileProcessing(file, startTime, false, error.message);
throw error; throw error;
} finally {
const processingTime = performance.now() - startTime;
// Capture analytics
const pageCount = file && file.type === 'application/pdf' ? await getPDFPageCount(file) : null;
posthog.capture('file_processing', {
success: success,
file_type: file ? file.type || 'unknown' : 'unknown',
file_size: file ? file.size : 0,
processing_time: processingTime,
error_message: errorMessage,
pdf_pages: pageCount
});
} }
} }
function getFilenameFromContentDisposition(contentDisposition) { function getFilenameFromContentDisposition(contentDisposition) {
let filename; let filename;
if (contentDisposition && contentDisposition.indexOf("attachment") !== -1) { if (contentDisposition && contentDisposition.indexOf("attachment") !== -1) {
@@ -204,8 +224,7 @@
if (considerViewOptions) { if (considerViewOptions) {
if (downloadOption === "sameWindow") { if (downloadOption === "sameWindow") {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.location.href = url; return { filename, blob, url };
return;
} else if (downloadOption === "newWindow") { } else if (downloadOption === "newWindow") {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); window.open(url, "_blank");
@@ -343,14 +362,14 @@
if(editSectionElement){ if(editSectionElement){
editSectionElement.style.display = "none"; editSectionElement.style.display = "none";
} }
let cropPdfCanvas = document.querySelector("#cropPdfCanvas"); let cropPdfCanvas = document.querySelector("#crop-pdf-canvas");
let overlayCanvas = document.querySelector("#overlayCanvas"); let overlayCanvas = document.querySelector("#overlayCanvas");
if(cropPdfCanvas && overlayCanvas){ if(cropPdfCanvas && overlayCanvas){
cropPdfCanvas.width = 0; cropPdfCanvas.width = 0;
cropPdfCanvas.height = 0; cropPdfCanvas.heigth = 0;
overlayCanvas.width = 0; overlayCanvas.width = 0;
overlayCanvas.height = 0; overlayCanvas.heigth = 0;
} }
} else{ } else{
console.log("Disabled for 'Merge'"); console.log("Disabled for 'Merge'");

View File

@@ -9,81 +9,81 @@ const DraggableUtils = {
init() { init() {
interact(".draggable-canvas") interact(".draggable-canvas")
.draggable({ .draggable({
listeners: { listeners: {
move: (event) => { move: (event) => {
const target = event.target; const target = event.target;
const x = (parseFloat(target.getAttribute("data-bs-x")) || 0) const x = (parseFloat(target.getAttribute("data-bs-x")) || 0)
+ event.dx; + event.dx;
const y = (parseFloat(target.getAttribute("data-bs-y")) || 0) const y = (parseFloat(target.getAttribute("data-bs-y")) || 0)
+ event.dy; + event.dy;
target.style.transform = `translate(${x}px, ${y}px)`; target.style.transform = `translate(${x}px, ${y}px)`;
target.setAttribute("data-bs-x", x); target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y); target.setAttribute("data-bs-y", y);
this.onInteraction(target); this.onInteraction(target);
//update the last interacted element //update the last interacted element
this.lastInteracted = event.target; this.lastInteracted = event.target;
},
}, },
}) },
.resizable({ })
edges: { left: true, right: true, bottom: true, top: true }, .resizable({
listeners: { edges: { left: true, right: true, bottom: true, top: true },
move: (event) => { listeners: {
var target = event.target; move: (event) => {
var x = parseFloat(target.getAttribute("data-bs-x")) || 0; var target = event.target;
var y = parseFloat(target.getAttribute("data-bs-y")) || 0; var x = parseFloat(target.getAttribute("data-bs-x")) || 0;
var y = parseFloat(target.getAttribute("data-bs-y")) || 0;
// check if control key is pressed // check if control key is pressed
if (event.ctrlKey) { if (event.ctrlKey) {
const aspectRatio = target.offsetWidth / target.offsetHeight; const aspectRatio = target.offsetWidth / target.offsetHeight;
// preserve aspect ratio // preserve aspect ratio
let width = event.rect.width; let width = event.rect.width;
let height = event.rect.height; let height = event.rect.height;
if (Math.abs(event.deltaRect.width) >= Math.abs( if (Math.abs(event.deltaRect.width) >= Math.abs(
event.deltaRect.height)) { event.deltaRect.height)) {
height = width / aspectRatio; height = width / aspectRatio;
} else { } else {
width = height * aspectRatio; width = height * aspectRatio;
}
event.rect.width = width;
event.rect.height = height;
} }
target.style.width = event.rect.width + "px"; event.rect.width = width;
target.style.height = event.rect.height + "px"; event.rect.height = height;
}
// translate when resizing from top or left edges target.style.width = event.rect.width + "px";
x += event.deltaRect.left; target.style.height = event.rect.height + "px";
y += event.deltaRect.top;
target.style.transform = "translate(" + x + "px," + y + "px)"; // translate when resizing from top or left edges
x += event.deltaRect.left;
y += event.deltaRect.top;
target.setAttribute("data-bs-x", x); target.style.transform = "translate(" + x + "px," + y + "px)";
target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + "\u00D7" target.setAttribute("data-bs-x", x);
target.setAttribute("data-bs-y", y);
target.textContent = Math.round(event.rect.width) + "\u00D7"
+ Math.round(event.rect.height); + Math.round(event.rect.height);
this.onInteraction(target); this.onInteraction(target);
},
}, },
},
modifiers: [ modifiers: [
interact.modifiers.restrictSize({ interact.modifiers.restrictSize({
min: { width: 5, height: 5 }, min: {width: 5, height: 5},
}), }),
], ],
inertia: true, inertia: true,
}); });
//Arrow key Support for Add-Image and Sign pages //Arrow key Support for Add-Image and Sign pages
if (window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) { if(window.location.pathname.endsWith('sign') || window.location.pathname.endsWith('add-image')) {
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
//Check for last interacted element //Check for last interacted element
if (!this.lastInteracted) { if (!this.lastInteracted){
return; return;
} }
// Get the currently selected element // Get the currently selected element
@@ -288,7 +288,7 @@ const DraggableUtils = {
} }
}, },
parseTransform(element) { }, parseTransform(element) {},
async getOverlayedPdfDocument() { async getOverlayedPdfDocument() {
const pdfBytes = await this.pdfDoc.getData(); const pdfBytes = await this.pdfDoc.getData();
const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, { const pdfDocModified = await PDFLib.PDFDocument.load(pdfBytes, {
@@ -308,7 +308,6 @@ const DraggableUtils = {
const offsetWidth = pagesMap[pageIdx + "-offsetWidth"]; const offsetWidth = pagesMap[pageIdx + "-offsetWidth"];
const offsetHeight = pagesMap[pageIdx + "-offsetHeight"]; const offsetHeight = pagesMap[pageIdx + "-offsetHeight"];
for (const draggableData of draggablesData) { for (const draggableData of draggablesData) {
// embed the draggable canvas // embed the draggable canvas
const draggableElement = draggableData.element; const draggableElement = draggableData.element;
@@ -325,24 +324,6 @@ const DraggableUtils = {
width: draggableData.offsetWidth, width: draggableData.offsetWidth,
height: draggableData.offsetHeight, height: draggableData.offsetHeight,
}; };
//Auxiliary variables
let widthAdjusted = page.getWidth();
let heightAdjusted = page.getHeight();
const rotation = page.getRotation();
//Normalizing angle
let normalizedAngle = rotation.angle % 360;
if (normalizedAngle < 0) {
normalizedAngle += 360;
}
//Changing the page dimension if the angle is 90 or 270
if (normalizedAngle === 90 || normalizedAngle === 270) {
let widthTemp = widthAdjusted;
widthAdjusted = heightAdjusted;
heightAdjusted = widthTemp;
}
const draggablePositionRelative = { const draggablePositionRelative = {
x: draggablePositionPixels.x / offsetWidth, x: draggablePositionPixels.x / offsetWidth,
y: draggablePositionPixels.y / offsetHeight, y: draggablePositionPixels.y / offsetHeight,
@@ -350,36 +331,18 @@ const DraggableUtils = {
height: draggablePositionPixels.height / offsetHeight, height: draggablePositionPixels.height / offsetHeight,
}; };
const draggablePositionPdf = { const draggablePositionPdf = {
x: draggablePositionRelative.x * widthAdjusted, x: draggablePositionRelative.x * page.getWidth(),
y: draggablePositionRelative.y * heightAdjusted, y: draggablePositionRelative.y * page.getHeight(),
width: draggablePositionRelative.width * widthAdjusted, width: draggablePositionRelative.width * page.getWidth(),
height: draggablePositionRelative.height * heightAdjusted, height: draggablePositionRelative.height * page.getHeight(),
}; };
//Defining the image if the page has a 0-degree angle
let x = draggablePositionPdf.x
let y = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height
//Defining the image position if it is at other angles
if (normalizedAngle === 90) {
x = draggablePositionPdf.y + draggablePositionPdf.height;
y = draggablePositionPdf.x;
} else if (normalizedAngle === 180) {
x = widthAdjusted - draggablePositionPdf.x;
y = draggablePositionPdf.y + draggablePositionPdf.height;
} else if (normalizedAngle === 270) {
x = heightAdjusted - draggablePositionPdf.y - draggablePositionPdf.height;
y = widthAdjusted - draggablePositionPdf.x;
}
// draw the image // draw the image
page.drawImage(pdfImageObject, { page.drawImage(pdfImageObject, {
x: x, x: draggablePositionPdf.x,
y: y, y: page.getHeight() - draggablePositionPdf.y - draggablePositionPdf.height,
width: draggablePositionPdf.width, width: draggablePositionPdf.width,
height: draggablePositionPdf.height, height: draggablePositionPdf.height,
rotate: rotation
}); });
} }
} }

View File

@@ -76,7 +76,7 @@ async function checkForUpdate() {
document.getElementById("update-btn").style.display = "block"; document.getElementById("update-btn").style.display = "block";
} }
if (updateLink !== null) { if (updateLink !== null) {
document.getElementById("app-update").innerHTML = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>'); document.getElementById("app-update").innerText = updateAvailable.replace("{0}", '<b>' + currentVersion + '</b>').replace("{1}", '<b>' + latestVersion + '</b>');
if (updateLink.classList.contains("visually-hidden")) { if (updateLink.classList.contains("visually-hidden")) {
updateLink.classList.remove("visually-hidden"); updateLink.classList.remove("visually-hidden");
} }

View File

@@ -23,26 +23,23 @@
</form> </form>
<p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p> <p id="instruction-text" style="margin: 0; display: none" th:text="#{PDFToCSV.prompt}"></p>
<div style="position: relative; width: auto;" id="canvasesContainer"> <div style="position: relative; display: inline-block;">
<div> <div>
<div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container"> <div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container">
<button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &lt; </button> <button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &lt; </button>
<button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &gt; </button> <button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff; ;border: none;outline: none; border-radius: 4px;'> &gt; </button>
</div> </div>
<canvas id="cropPdfCanvas" style="width: 100%"></canvas> <canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas>
</div> </div>
<canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2; width: 100%"></canvas> <canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas>
</div> </div>
<script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script> <script type="module" th:src="@{'/pdfjs-legacy/pdf.mjs'}"></script>
<script> <script>
let pdfCanvas = document.getElementById('cropPdfCanvas'); let pdfCanvas = document.getElementById('crop-pdf-canvas');
let overlayCanvas = document.getElementById('overlayCanvas'); let overlayCanvas = document.getElementById('overlayCanvas');
let canvasesContainer = document.getElementById('canvasesContainer');
canvasesContainer.style.display = "none";
// let paginationBtnContainer = ; // let paginationBtnContainer = ;
let context = pdfCanvas.getContext('2d'); let context = pdfCanvas.getContext('2d');
let overlayContext = overlayCanvas.getContext('2d');
let btn1Object = document.getElementById('previous-page-btn'); let btn1Object = document.getElementById('previous-page-btn');
let btn2Object = document.getElementById('next-page-btn'); let btn2Object = document.getElementById('next-page-btn');
@@ -63,8 +60,6 @@
let rectWidth = 0; let rectWidth = 0;
let rectHeight = 0; let rectHeight = 0;
let timeId = null; // timeout id for resizing canvases event
btn1Object.addEventListener('click',function (e){ btn1Object.addEventListener('click',function (e){
if (currentPage !== 1) { if (currentPage !== 1) {
currentPage = currentPage - 1; currentPage = currentPage - 1;
@@ -107,13 +102,14 @@
} }
}); });
function renderPageFromFile(file) { fileInput.addEventListener('change', function(e) {
file = e.target.files[0];
if (file.type === 'application/pdf') { if (file.type === 'application/pdf') {
let reader = new FileReader(); let reader = new FileReader();
reader.onload = function (ev) { reader.onload = function(ev) {
let typedArray = new Uint8Array(reader.result); let typedArray = new Uint8Array(reader.result);
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs'; pdfjsLib.GlobalWorkerOptions.workerSrc = './pdfjs-legacy/pdf.worker.mjs';
pdfjsLib.getDocument(typedArray).promise.then(function (pdf) { pdfjsLib.getDocument(typedArray).promise.then(function(pdf) {
pdfDoc = pdf; pdfDoc = pdf;
totalPages = pdf.numPages; totalPages = pdf.numPages;
renderPage(currentPage); renderPage(currentPage);
@@ -121,37 +117,9 @@
pageId.value = currentPage; pageId.value = currentPage;
}; };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
document.getElementById("pagination-button-container").style.display = "flex"; document.getElementById("pagination-button-container").style.display="flex";
document.getElementById("instruction-text").style.display = "block"; document.getElementById("instruction-text").style.display="block";
} }
}
window.addEventListener("resize", function() {
clearTimeout(timeId);
timeId = setTimeout(function () {
if (fileInput.files.length == 0) return;
let canvasesContainer = document.getElementById('canvasesContainer');
let containerRect = canvasesContainer.getBoundingClientRect();
context.clearRect(0, 0, pdfCanvas.width, pdfCanvas.height);
overlayContext.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
pdfCanvas.width = containerRect.width;
pdfCanvas.height = containerRect.height;
overlayCanvas.width = containerRect.width;
overlayCanvas.height = containerRect.height;
let file = fileInput.files[0];
renderPageFromFile(file);
}, 1000);
});
fileInput.addEventListener('change', function(e) {
canvasesContainer.style.display = "block"; // set for visual purposes
file = e.target.files[0];
renderPageFromFile(file);
}); });
function renderPage(pageNumber) { function renderPage(pageNumber) {

View File

@@ -330,7 +330,7 @@
<span class="material-symbols-rounded" id="dark-mode-icon"> <span class="material-symbols-rounded" id="dark-mode-icon">
dark_mode dark_mode
</span> </span>
<span class="icon-text icon-hide" id="dark-mode-text" th:data-text="#{navbar.darkmode}" th:text="#{navbar.darkmode}"></span> <span class="icon-text icon-hide tooltip-text" id="dark-mode-text" th:data-text="#{navbar.darkmode}" th:text="#{navbar.darkmode}"></span>
</a> </a>
</li> </li>
<li class="nav-item dropdown"> <li class="nav-item dropdown">
@@ -380,7 +380,7 @@
<span class="material-symbols-rounded"> <span class="material-symbols-rounded">
settings settings
</span> </span>
<span class="icon-text icon-hide" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span> <span class="icon-text icon-hide tooltip-text" th:data-text="#{navbar.settings}" th:text="#{navbar.settings}"></span>
</a> </a>
</li> </li>
</ul> </ul>
@@ -411,7 +411,7 @@
th:title="#{visitGithub}"> th:title="#{visitGithub}">
<img th:src="@{'/images/github.svg'}" alt="github"> <img th:src="@{'/images/github.svg'}" alt="github">
</a> </a>
<a href="https://hub.docker.com/r/stirlingtools/stirling-pdf" class="mx-1" role="button" target="_blank"th:title="#{seeDockerHub}"> <a href="https://hub.docker.com/r/frooodle/s-pdf" class="mx-1" role="button" target="_blank"th:title="#{seeDockerHub}">
<img th:src="@{'/images/docker.svg'}" alt="docker"> <img th:src="@{'/images/docker.svg'}" alt="docker">
</a> </a>
<a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank" th:title="#{joinDiscord}"> <a href="https://discord.gg/Cn8pWhQRxZ" class="mx-1" role="button" target="_blank" th:title="#{joinDiscord}">

View File

@@ -83,6 +83,9 @@
</span> </span>
</button> </button>
</div> </div>
<div class="mb-3">
<button type="button" id="resetFileInputBtn" class="btn btn-danger" onclick="removeAllElements()" th:text="#{reset}">Reset</button>
</div>
<div id="selected-pages-display" class="selected-pages-container hidden"> <div id="selected-pages-display" class="selected-pages-container hidden">
<div style="display:flex; height:3rem; margin-right:1rem"> <div style="display:flex; height:3rem; margin-right:1rem">
<h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected <h5 th:text="#{multiTool.selectedPages}" style="white-space: nowrap; margin-right: 1rem;">Selected

View File

@@ -49,7 +49,7 @@ check_health() {
build_and_test() { build_and_test() {
local version_tag="alpha" local version_tag="alpha"
local dockerfile_name="./Dockerfile" local dockerfile_name="./Dockerfile"
local image_base="stirlingtools/stirling-pdf" local image_base="frooodle/s-pdf"
local security_suffix="" local security_suffix=""
local docker_compose_base="./exampleYmlFiles/docker-compose-latest" local docker_compose_base="./exampleYmlFiles/docker-compose-latest"
local compose_suffix=".yml" local compose_suffix=".yml"