Compare commits

..

6 Commits

Author SHA1 Message Date
Anthony Stirling
217bba3a9d Merge branch 'main' into add-elements 2023-12-30 20:17:41 +00:00
Anthony Stirling
0e9f52bca2 Merge branch 'main' into add-elements 2023-12-28 23:52:45 +00:00
Anthony Stirling
4f6286845d Api fix 2023-12-28 22:52:53 +00:00
Anthony Stirling
5d611a2fa3 Merge remote-tracking branch 'origin/main' into add-elements 2023-12-27 15:26:10 +00:00
Saud Fatayerji
86984f2142 Add elements first draft 2023-09-28 19:31:43 +03:00
Saud Fatayerji
e998426b3b Add elements demo (WIP) 2023-09-04 17:07:29 -07:00
470 changed files with 31216 additions and 49793 deletions

View File

@@ -1,5 +1,2 @@
# Formatting # Formatting
5f771b785130154ed47952635b7acef371ffe0ec 5f771b785130154ed47952635b7acef371ffe0ec
# Normalize files
55d4fda01b2f39f5b7d7b4fda5214bd7ff0fd5dd

2
.gitattributes vendored
View File

@@ -1,5 +1,3 @@
* text=auto eol=lf
# Ignore all JavaScript files in a directory # Ignore all JavaScript files in a directory
src/main/resources/static/pdfjs/* linguist-vendored src/main/resources/static/pdfjs/* linguist-vendored
src/main/resources/static/pdfjs/** linguist-vendored src/main/resources/static/pdfjs/** linguist-vendored

View File

@@ -9,7 +9,3 @@ updates:
directory: "/" # Location of package manifests directory: "/" # Location of package manifests
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "docker"
directory: "/" # Location of Dockerfile
schedule:
interval: "weekly"

View File

@@ -1,18 +1,4 @@
# Description # License Agreement for Contributions
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
Please provide a summary of the changes, including relevant motivation and context.
Closes #(issue_number)
## Checklist:
- [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] My changes generate no new warnings
## Contributor License Agreement
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.
(This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license) (This does not change the general open-source nature of Stirling-PDF, simply moving from one license to another license)

View File

@@ -1,51 +0,0 @@
import sys
def find_duplicate_keys(file_path):
"""
Finds duplicate keys in a properties file and returns their occurrences.
This function reads a properties file, identifies any keys that occur more than
once, and returns a dictionary with these keys and the line numbers of their occurrences.
Parameters:
file_path (str): The path to the properties file to be checked.
Returns:
dict: A dictionary where each key is a duplicated key in the file, and the value is a list
of line numbers where the key occurs.
"""
with open(file_path, "r", encoding="utf-8") as file:
lines = file.readlines()
keys = {}
duplicates = {}
for line_number, line in enumerate(lines, start=1):
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key = line.split("=", 1)[0].strip()
if key in keys:
# If the key already exists, add the current line number
duplicates.setdefault(key, []).append(line_number)
# Also add the first instance of the key if not already done
if keys[key] not in duplicates[key]:
duplicates[key].insert(0, keys[key])
else:
# Store the line number of the first instance of the key
keys[key] = line_number
return duplicates
if __name__ == "__main__":
failed = False
for ar in sys.argv[1:]:
duplicates = find_duplicate_keys(ar)
if duplicates:
for key, lines in duplicates.items():
lines_str = ", ".join(map(str, lines))
print(f"{key} duplicated in {ar} on lines {lines_str}")
failed = True
if failed:
sys.exit(1)

View File

@@ -1,84 +0,0 @@
"""check_tabulator.py"""
import argparse
import sys
def check_tabs(file_path):
"""
Checks for tabs in the specified file.
Args:
file_path (str): The path to the file to be checked.
Returns:
bool: True if tabs are found, False otherwise.
"""
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()
if "\t" in content:
print(f"Tab found in {file_path}")
return True
return False
def replace_tabs_with_spaces(file_path, replace_with=" "):
"""
Replaces tabs with a specified number of spaces in the file.
Args:
file_path (str): The path to the file where tabs will be replaced.
replace_with (str): The character(s) to replace tabs with. Defaults to two spaces.
"""
with open(file_path, "r", encoding="utf-8") as file:
content = file.read()
updated_content = content.replace("\t", replace_with)
with open(file_path, "w", encoding="utf-8") as file:
file.write(updated_content)
def main():
"""
Main function to replace tabs with spaces in the provided files.
The replacement character and files to check are taken from command line arguments.
"""
# Create ArgumentParser instance
parser = argparse.ArgumentParser(
description="Replace tabs in files with specified characters."
)
# Define optional argument `--replace_with`
parser.add_argument(
"--replace_with",
default=" ",
help="Character(s) to replace tabs with. Default is two spaces.",
)
# Define argument for file paths
parser.add_argument("files", metavar="FILE", nargs="+", help="Files to process.")
# Parse arguments
args = parser.parse_args()
# Extract replacement characters and files from the parsed arguments
replace_with = args.replace_with
files_checked = args.files
error = False
for file_path in files_checked:
if check_tabs(file_path):
replace_tabs_with_spaces(file_path, replace_with)
error = True
if error:
print("Error: Originally found tabs in HTML files, now replaced.")
sys.exit(1)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,67 +0,0 @@
import re
import yaml
# Paths to the files
chart_yaml_path = "chart/stirling-pdf/Chart.yaml"
gradle_path = "build.gradle"
def get_chart_version(path):
"""
Reads the appVersion from Chart.yaml.
Args:
path (str): The file path to the Chart.yaml.
Returns:
str: The appVersion if found, otherwise an empty string.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
return chart_yaml.get("appVersion", "")
def get_gradle_version(path):
"""
Extracts the version from build.gradle.
Args:
path (str): The file path to the build.gradle.
Returns:
str: The version if found, otherwise an empty string.
"""
with open(path, encoding="utf-8") as file:
for line in file:
if "version =" in line:
# Extracts the value after 'version ='
return re.search(r'version\s*=\s*[\'"](.+?)[\'"]', line).group(1)
return ""
def update_chart_version(path, new_version):
"""
Updates the appVersion in Chart.yaml with a new version.
Args:
path (str): The file path to the Chart.yaml.
new_version (str): The new version to update to.
"""
with open(path, encoding="utf-8") as file:
chart_yaml = yaml.safe_load(file)
chart_yaml["appVersion"] = new_version
with open(path, "w", encoding="utf-8") as file:
yaml.safe_dump(chart_yaml, file)
# Main logic
chart_version = get_chart_version(chart_yaml_path)
gradle_version = get_gradle_version(gradle_path)
if chart_version != gradle_version:
print(
f"Versions do not match. Updating Chart.yaml from {chart_version} to {gradle_version}."
)
update_chart_version(chart_yaml_path, gradle_version)
else:
print("Versions match. No update required.")

View File

@@ -2,15 +2,9 @@ name: "Build repo"
on: on:
push: push:
branches: ["main"] branches: [ "main" ]
paths-ignore:
- ".github/**"
- "**/*.md"
pull_request: pull_request:
branches: ["main"] branches: [ "main" ]
paths-ignore:
- ".github/**"
- "**/*.md"
jobs: jobs:
build: build:
@@ -25,18 +19,16 @@ jobs:
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v3
with: with:
java-version: "17" java-version: '17'
distribution: "temurin" distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/gradle-build-action@v2.4.2
with: with:
gradle-version: 7.6 gradle-version: 7.6
arguments: build --no-build-cache
- name: Build with Gradle
run: ./gradlew build --no-build-cache

View File

@@ -1,62 +0,0 @@
name: License Report Workflow
on:
push:
branches:
- main
paths:
- "build.gradle"
permissions:
contents: write
pull-requests: write
jobs:
generate-license-report:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- uses: gradle/actions/setup-gradle@v3
- name: Run Gradle Command
run: ./gradlew clean generateLicenseReport
- name: Move and Rename License File
run: |
mv build/reports/dependency-license/index.json src/main/resources/static/3rdPartyLicenses.json
- name: Set up git config
run: |
git config --global user.email "GitHub Action <action@github.com>"
git config --global user.name "GitHub Action <action@github.com>"
- name: Run git add
run: |
git add src/main/resources/static/3rdPartyLicenses.json
git diff --staged --quiet || echo "CHANGES_DETECTED=true" >> $GITHUB_ENV
- name: Create Pull Request
if: env.CHANGES_DETECTED == 'true'
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "Update 3rd Party Licenses"
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: update-3rd-party-licenses
title: "Update 3rd Party Licenses"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true

View File

@@ -0,0 +1,3 @@
# License Agreement for Contributions
By submitting this pull request, I acknowledge and agree that my contributions will be included in Stirling-PDF and that they can be relicensed in the future under MPL 2.0 (Mozilla Public License Version 2.0) license.
(This does not change the open-source nature of Stirling-PDF, simply moving from one license to another license)

View File

@@ -3,110 +3,146 @@ name: Push Docker Image with VersionNumber
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- master - master
- main - main
permissions:
contents: read
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4
- name: Set up JDK 17 - uses: actions/checkout@v3.5.2
uses: actions/setup-java@v4
with: - name: Set up JDK 17
java-version: "17" uses: actions/setup-java@v3.11.0
distribution: "temurin" with:
java-version: '17'
distribution: 'temurin'
- uses: gradle/actions/setup-gradle@v3 - uses: gradle/gradle-build-action@v2.4.2
with: env:
gradle-version: 7.6 DOCKER_ENABLE_SECURITY: false
with:
gradle-version: 7.6
arguments: clean build
- name: Run Gradle Command - name: Make Gradle wrapper executable
run: ./gradlew clean build run: chmod +x gradlew
env:
DOCKER_ENABLE_SECURITY: false - name: Get version number
id: versionNumber
run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Set up Docker Buildx - name: Login to GitHub Container Registry
id: buildx uses: docker/login-action@v2.1.0
uses: docker/setup-buildx-action@v3 with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Get version number - name: Convert repository owner to lowercase
id: versionNumber id: repoowner
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "::set-output name=lowercase::$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')"
- name: Generate tags
id: meta
uses: docker/metadata-action@v4.4.0
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Login to Docker Hub - name: Set up QEMU
uses: docker/login-action@v3 uses: docker/setup-qemu-action@v2.1.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_API }}
- name: Login to GitHub Container Registry - name: Set up Docker Buildx
uses: docker/login-action@v3 uses: docker/setup-buildx-action@v2.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Set up QEMU - name: Build and push main Dockerfile
uses: docker/setup-qemu-action@v3 uses: docker/build-push-action@v4.0.0
with:
context: .
dockerfile: ./Dockerfile
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
- name: Convert repository owner to lowercase
id: repoowner
run: echo "lowercase=$(echo ${{ github.repository_owner }} | awk '{print tolower($0)}')" >> $GITHUB_OUTPUT
- name: Generate tags
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
tags: |
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }},enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }}
type=raw,value=alpha,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push main Dockerfile - name: Generate tags ultra-lite
uses: docker/build-push-action@v5 id: meta2
with: uses: docker/metadata-action@v4.4.0
builder: ${{ steps.buildx.outputs.name }} if: github.ref != 'refs/heads/main'
context: . with:
file: ./Dockerfile images: |
push: true ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
cache-from: type=gha ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
cache-to: type=gha,mode=max tags: |
tags: ${{ steps.meta.outputs.tags }} type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
labels: ${{ steps.meta.outputs.labels }} type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }}
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
- name: Generate tags ultra-lite - name: Build and push Dockerfile-ultra-lite
id: meta2 uses: docker/build-push-action@v4.0.0
uses: docker/metadata-action@v5 if: github.ref != 'refs/heads/main'
if: github.ref != 'refs/heads/main' with:
with: context: .
images: | file: ./Dockerfile-ultra-lite
${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf push: true
ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf cache-from: type=gha
tags: | cache-to: type=gha,mode=max
type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} tags: ${{ steps.meta2.outputs.tags }}
type=raw,value=latest-ultra-lite,enable=${{ github.ref == 'refs/heads/master' }} labels: ${{ steps.meta2.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
- name: Build and push Dockerfile-ultra-lite
uses: docker/build-push-action@v5
if: github.ref != 'refs/heads/main' - name: Generate tags lite
with: id: meta3
context: . uses: docker/metadata-action@v4.4.0
file: ./Dockerfile-ultra-lite if: github.ref != 'refs/heads/main'
push: true with:
cache-from: type=gha images: |
cache-to: type=gha,mode=max ${{ secrets.DOCKER_HUB_USERNAME }}/s-pdf
tags: ${{ steps.meta2.outputs.tags }} ghcr.io/${{ steps.repoowner.outputs.lowercase }}/s-pdf
labels: ${{ steps.meta2.outputs.labels }} tags: |
build-args: VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }} type=raw,value=${{ steps.versionNumber.outputs.versionNumber }}-lite,enable=${{ github.ref == 'refs/heads/master' }}
platforms: linux/amd64,linux/arm64/v8 type=raw,value=latest-lite,enable=${{ github.ref == 'refs/heads/master' }}
- name: Build and push Dockerfile-lite
uses: docker/build-push-action@v4.0.0
if: github.ref != 'refs/heads/main'
with:
context: .
file: ./Dockerfile-lite
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta3.outputs.tags }}
labels: ${{ steps.meta3.outputs.labels }}
build-args:
VERSION_TAG=${{ steps.versionNumber.outputs.versionNumber }}
platforms: linux/amd64,linux/arm64/v8
- name: Build and Push Helm Chart
run: |
helm package chart/stirling-pdf
helm push stirling-pdf-chart-1.0.0.tgz oci://registry-1.docker.io/frooodle

View File

@@ -1,12 +1,9 @@
name: Release Artifacts name: Release Artifacts
on: on:
workflow_dispatch: release:
release:
types: [created] types: [created]
permissions:
contents: write
packages: write
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -15,61 +12,44 @@ jobs:
enable_security: [true, false] enable_security: [true, false]
include: include:
- enable_security: true - enable_security: true
file_suffix: "-with-login" file_suffix: '-with-login'
- enable_security: false - enable_security: false
file_suffix: "" file_suffix: ''
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3.5.2
- name: Set up JDK 17
uses: actions/setup-java@v3.11.0
with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Set up JDK 17 - name: Generate jar (With Security=${{ matrix.enable_security }})
uses: actions/setup-java@v4 run: ./gradlew clean createExe
with: env:
java-version: "17" DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }}
distribution: "temurin"
- uses: gradle/actions/setup-gradle@v3 - name: Upload binaries to release
with: uses: svenstaro/upload-release-action@v2
gradle-version: 7.6 with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate jar (With Security=${{ matrix.enable_security }}) file: ./build/launch4j/Stirling-PDF.exe
run: ./gradlew clean createExe asset_name: Stirling-PDF${{ matrix.file_suffix }}.exe
env: tag: ${{ github.ref }}
DOCKER_ENABLE_SECURITY: ${{ matrix.enable_security }} overwrite: true
- name: Get version number - name: Get version number
id: versionNumber id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT run: echo "::set-output name=versionNumber::$(./gradlew printVersion --quiet | tail -1)"
- name: Rename binarie - name: Upload jar binaries to release
if: matrix.file_suffix != '' uses: svenstaro/upload-release-action@v2
run: cp ./build/launch4j/Stirling-PDF.exe ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Assets binarie file: ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar
uses: actions/upload-artifact@v4 asset_name: Stirling-PDF${{ matrix.file_suffix }}.jar
with: tag: ${{ github.ref }}
path: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe overwrite: true
name: Stirling-PDF${{ matrix.file_suffix }}.exe
overwrite: true
retention-days: 1
if-no-files-found: error
- name: Upload binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./build/launch4j/Stirling-PDF${{ matrix.file_suffix }}.exe
- name: Rename jar binaries
run: cp ./build/libs/Stirling-PDF-${{ steps.versionNumber.outputs.versionNumber }}.jar ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
- name: Upload Assets jar binaries
uses: actions/upload-artifact@v4
with:
path: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar
name: Stirling-PDF${{ matrix.file_suffix }}.jar
overwrite: true
retention-days: 1
if-no-files-found: error
- name: Upload jar binaries to release
uses: softprops/action-gh-release@v2
with:
files: ./build/libs/Stirling-PDF${{ matrix.file_suffix }}.jar

View File

@@ -3,37 +3,35 @@ name: Update Swagger
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- master - master
jobs: jobs:
push: push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4
- name: Set up JDK 17 - uses: actions/checkout@v3.5.2
uses: actions/setup-java@v4
with: - name: Set up JDK 17
java-version: "17" uses: actions/setup-java@v3.11.0
distribution: "temurin" with:
java-version: '17'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- uses: gradle/actions/setup-gradle@v3 - name: Generate Swagger documentation
run: ./gradlew generateOpenApiDocs
- name: Generate Swagger documentation - name: Upload Swagger Documentation to SwaggerHub
run: ./gradlew generateOpenApiDocs run: ./gradlew swaggerhubUpload
env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
- name: Upload Swagger Documentation to SwaggerHub - name: Set API version as published and default on SwaggerHub
run: ./gradlew swaggerhubUpload run: |
env: curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }} env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}
- name: Get version number
id: versionNumber
run: echo "versionNumber=$(./gradlew printVersion --quiet | tail -1)" >> $GITHUB_OUTPUT
- name: Set API version as published and default on SwaggerHub
run: |
curl -X PUT -H "Authorization: ${SWAGGERHUB_API_KEY}" "https://api.swaggerhub.com/apis/Frooodle/Stirling-PDF/${{ steps.versionNumber.outputs.versionNumber }}/settings/lifecycle" -H "accept: application/json" -H "Content-Type: application/json" -d "{\"published\":true,\"default\":true}"
env:
SWAGGERHUB_API_KEY: ${{ secrets.SWAGGERHUB_API_KEY }}

View File

@@ -1,90 +0,0 @@
name: Sync Files
on:
push:
branches:
- main
paths:
- "build.gradle"
- "src/main/resources/messages_*.properties"
- "scripts/translation_status.toml"
permissions:
contents: write
pull-requests: write
jobs:
sync-versions:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:
python-version: "3.x"
- name: Install dependencies
run: pip install pyyaml
- name: Sync versions
run: python .github/scripts/gradle_to_chart.py
- name: Set up git config
run: |
git config --global user.email "GitHub Action <action@github.com>"
git config --global user.name "GitHub Action <action@github.com>"
- name: Run git add
run: |
git add .
git diff --staged --quiet || git commit -m ":floppy_disk: Sync Versions
> Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: sync_version
title: ":floppy_disk: Update Version"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true
sync-readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.1.1
- name: Set up Python
uses: actions/setup-python@v5.1.0
with:
python-version: "3.x"
- name: Install dependencies
run: pip install tomlkit
- name: Sync README
run: python scripts/counter_translation.py
- name: Set up git config
run: |
git config --global user.email "GitHub Action <action@github.com>"
git config --global user.name "GitHub Action <action@github.com>"
- name: Run git add
run: |
git add .
git diff --staged --quiet || git commit -m ":memo: Sync README
> Made via sync_files.yml" || echo "no changes"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: Update files
committer: GitHub Action <action@github.com>
author: GitHub Action <action@github.com>
signoff: true
branch: sync_readme
title: ":memo: Update README: Translation Progress Table"
body: |
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
draft: false
delete-branch: true

View File

@@ -1,38 +0,0 @@
name: Docker Compose Tests
on:
pull_request:
paths:
- "src/**"
- "**.gradle"
- "!src/main/java/resources/messages*"
- "exampleYmlFiles/**"
- "Dockerfile"
- "Dockerfile**"
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: "17"
distribution: "adopt"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Install Docker Compose
run: |
sudo curl -SL "https://github.com/docker/compose/releases/download/v2.26.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
# sudo chmod +x /usr/local/bin/docker-compose
- name: Run Docker Compose Tests
run: |
chmod +x ./test.sh
./test.sh

252
.gitignore vendored
View File

@@ -1,127 +1,127 @@
### Eclipse ### ### Eclipse ###
.metadata .metadata
bin/ bin/
tmp/ tmp/
*.tmp *.tmp
*.bak *.bak
*.swp *.swp
*~.nib *~.nib
local.properties local.properties
.settings/ .settings/
.loadpath .loadpath
.recommenders .recommenders
.classpath .classpath
.project .project
version.properties version.properties
pipeline/watchedFolders/ pipeline/watchedFolders/
pipeline/finishedFolders/ pipeline/finishedFolders/
#### Stirling-PDF Files ### #### Stirling-PDF Files ###
customFiles/ customFiles/
configs/ configs/
watchedFolders/ watchedFolders/
# Gradle # Gradle
.gradle .gradle
.lock .lock
# External tool builders # External tool builders
.externalToolBuilders/ .externalToolBuilders/
# Locally stored "Eclipse launch configurations" # Locally stored "Eclipse launch configurations"
*.launch *.launch
# PyDev specific (Python IDE for Eclipse) # PyDev specific (Python IDE for Eclipse)
*.pydevproject *.pydevproject
# CDT-specific (C/C++ Development Tooling) # CDT-specific (C/C++ Development Tooling)
.cproject .cproject
# CDT- autotools # CDT- autotools
.autotools .autotools
# Java annotation processor (APT) # Java annotation processor (APT)
.factorypath .factorypath
# PDT-specific (PHP Development Tools) # PDT-specific (PHP Development Tools)
.buildpath .buildpath
# sbteclipse plugin # sbteclipse plugin
.target .target
# Tern plugin # Tern plugin
.tern-project .tern-project
# TeXlipse plugin # TeXlipse plugin
.texlipse .texlipse
# STS (Spring Tool Suite) # STS (Spring Tool Suite)
.springBeans .springBeans
# Code Recommenders # Code Recommenders
.recommenders/ .recommenders/
# Annotation Processing # Annotation Processing
.apt_generated/ .apt_generated/
.apt_generated_test/ .apt_generated_test/
# Scala IDE specific (Scala & Java development for Eclipse) # Scala IDE specific (Scala & Java development for Eclipse)
.cache-main .cache-main
.scala_dependencies .scala_dependencies
.worksheet .worksheet
# Uncomment this line if you wish to ignore the project description file. # Uncomment this line if you wish to ignore the project description file.
# Typically, this file would be tracked if it contains build/dependency configurations: # Typically, this file would be tracked if it contains build/dependency configurations:
#.project #.project
### Eclipse Patch ### ### Eclipse Patch ###
# Spring Boot Tooling # Spring Boot Tooling
.sts4-cache/ .sts4-cache/
### Git ### ### Git ###
# Created by git for backups. To disable backups in Git: # Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false # $ git config --global mergetool.keepBackup false
*.orig *.orig
# Created by git when using merge tools for conflicts # Created by git when using merge tools for conflicts
*.BACKUP.* *.BACKUP.*
*.BASE.* *.BASE.*
*.LOCAL.* *.LOCAL.*
*.REMOTE.* *.REMOTE.*
*_BACKUP_*.txt *_BACKUP_*.txt
*_BASE_*.txt *_BASE_*.txt
*_LOCAL_*.txt *_LOCAL_*.txt
*_REMOTE_*.txt *_REMOTE_*.txt
### Java ### ### Java ###
# Compiled class file # Compiled class file
*.class *.class
# Log file # Log file
*.log *.log
# BlueJ files # BlueJ files
*.ctxt *.ctxt
# Mobile Tools for Java (J2ME) # Mobile Tools for Java (J2ME)
.mtj.tmp/ .mtj.tmp/
# Package Files # # Package Files #
*.jar *.jar
*.war *.war
*.nar *.nar
*.ear *.ear
*.zip *.zip
*.tar.gz *.tar.gz
*.rar *.rar
*.db *.db
/build /build
/.vscode /.vscode
/.idea /.idea
# Ignore Mac DS_Store files # Ignore Mac DS_Store files
.DS_Store .DS_Store
**/.DS_Store **/.DS_Store

View File

@@ -1,37 +0,0 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.1
hooks:
- id: ruff
args:
- --fix
- --line-length=127
files: ^((.github/scripts)/.+)?[^/]+\.py$
- id: ruff-format
files: ^((.github/scripts)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
args:
- --ignore-words-list=
- --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2
files: \.(properties|html|css|js|py|md)$
exclude: (.vscode|.devcontainer|src/main/resources|Dockerfile)
- repo: local
hooks:
- id: check-duplicate-properties-keys
name: Check Duplicate Properties Keys
entry: python .github/scripts/check_duplicates.py
language: python
files: ^(src)/.+\.properties$
- repo: local
hooks:
- id: check-html-tabs
name: Check HTML for tabs
# args: ["--replace_with= "]
entry: python .github/scripts/check_tabulator.py
language: python
exclude: ^src/main/resources/static/pdfjs/
files: ^.*(\.html|\.css|\.js)$

1
CNAME Normal file
View File

@@ -0,0 +1 @@
stirlingtools.com

View File

@@ -1,44 +0,0 @@
# Contributing to Stirling-PDF
Thank you for your interest in contributing to Stirling-PDF! There are many ways to contribute other than writing code. For example, reporting bugs, creating suggestions, and adding or modifying translations.
## Issue Guidelines
Issues can be used to report bugs, request features, or ask questions. If you have a question, you could also ask us in our [Discord](https://discord.gg/FJUSXUSYec).
Before opening an issue, please check to make sure someone hasn't already opened an issue about it.
## Pull Requests
Before you start working on an issue, please comment on (or create) the issue and wait for it to be assigned to you. If someone has already been assigned but didn't have the time to work on it lately, please communicate with them and ask if they're still working on it. This is to avoid multiple people working on the same issue.
Once you have been assigned an issue, you can start working on it. When you are ready to submit your changes, open a pull request.
For a detailed pull request tutorial, see [this guide](https://www.digitalocean.com/community/tutorials/how-to-create-a-pull-request-on-github).
Please make sure your Pull Request adheres to the following guidelines:
- Use the PR template provided.
- Keep your Pull Request title succinct, detailed and to the point.
- Keep commits atomic. One commit should contain one change. If you want to make multiple changes, submit multiple Pull Requests.
- Commits should be clear, concise and easy to understand.
- References to the Issue number in the Pull Request and/or Commit message.
## Translations
If you would like to add or modify a translation, please see [How to add new languages to Stirling-PDF](HowToAddNewLanguage.md). Also, please create a Pull Request so others can use it!
## Docs
Documentation for Stirling-PDF is handled in a seperate repository. Please see [Docs repository](https://github.com/Stirling-Tools/Stirling-Tools.github.io) or use "edit this page"-button at the bottom of each page at [https://stirlingtools.com/docs/](https://stirlingtools.com/docs/).
## Fixing Bugs or Adding a New Feature
First, make sure you've read the section [Pull Requests](#pull-requests).
To build from source, please follow this [Guide](LocalRunGuide.md).
If, at any point of time, you have a question, please feel free to ask in the same issue thread or in our [Discord](https://discord.gg/FJUSXUSYec).
## License
By contributing to this project, you agree that your contributions will be licensed under the [GPL 3 License](LICENSE). You also acknowledge and agree that your contributions will be included in Stirling-PDF and that they can be relicensed in the future under the MPL 2.0 (Mozilla Public License Version 2.0) license.

View File

@@ -1,67 +1,47 @@
# Main stage # Use the base image
FROM alpine:3.20.0 FROM frooodle/stirling-pdf-base:version8
# Copy necessary files ARG VERSION_TAG
COPY scripts /scripts
COPY pipeline /pipeline # Set Environment Variables
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/ ENV DOCKER_ENABLE_SECURITY=false \
#COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/ HOME=/home/stirlingpdfuser \
COPY build/libs/*.jar app.jar VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
ARG VERSION_TAG # PUID=1000 \
# PGID=1000 \
# Set Environment Variables # UMASK=022 \
ENV DOCKER_ENABLE_SECURITY=false \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \ # Create user and group
HOME=/home/stirlingpdfuser \ ##RUN groupadd -g $PGID stirlingpdfgroup && \
PUID=1000 \ ## useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
PGID=1000 \ ## mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
UMASK=022
# Set up necessary directories and permissions
# JDK for app RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /logs /customFiles /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ ##&& \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /scripts /usr/share/fonts/opentype/noto /usr/share/tesseract-ocr /configs /customFiles && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \ ## chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/tesseract-ocr-original
apk upgrade --no-cache -a && \
apk add --no-cache \ # Copy necessary files
ca-certificates \ COPY ./scripts/* /scripts/
tzdata \ COPY ./pipeline/ /pipeline/
tini \ COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
bash \ COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
curl \ COPY build/libs/*.jar app.jar
shadow \
su-exec \ # Set font cache and permissions
openssl \ RUN fc-cache -f -v && chmod +x /scripts/*
openssl-dev \
openjdk21-jre \ ##&& \
# Doc conversion ## chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
libreoffice \ ## chmod +x /scripts/init.sh
# pdftohtml
poppler-utils \ # Expose necessary ports
# OCR MY PDF (unpaper for descew and other advanced featues) EXPOSE 8080
ocrmypdf \
tesseract-ocr-data-eng \ # Set user and run command
# CV ##USER stirlingpdfuser
py3-opencv \ ENTRYPOINT ["/scripts/init.sh"]
# python3/pip CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]
python3 && \
wget https://bootstrap.pypa.io/get-pip.py -qO - | python3 - --break-system-packages --no-cache-dir --upgrade && \
# uno unoconv and HTML
pip install --break-system-packages --no-cache-dir --upgrade unoconv WeasyPrint && \
mv /usr/share/tessdata /usr/share/tessdata-original && \
mkdir -p $HOME /configs /logs /customFiles /pipeline/watchedFolders /pipeline/finishedFolders && \
fc-cache -f -v && \
chmod +x /scripts/* && \
chmod +x /scripts/init.sh && \
# User permissions
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline && \
chown stirlingpdfuser:stirlingpdfgroup /app.jar && \
tesseract --list-langs
EXPOSE 8080/tcp
# Set user and run command
ENTRYPOINT ["tini", "--", "/scripts/init.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

65
Dockerfile-lite Normal file
View File

@@ -0,0 +1,65 @@
# Build jbig2enc in a separate stage
FROM bellsoft/liberica-openjdk-debian:17
ARG VERSION_TAG
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libreoffice-core \
libreoffice-common \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
unoconv && \
rm -rf /var/lib/apt/lists/*
# Set Environment Variables
ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
# PUID=1000 \
# PGID=1000 \
# UMASK=022 \
# Create user and group
#RUN groupadd -g $PGID stirlingpdfgroup && \
# useradd -u $PUID -g stirlingpdfgroup -s /bin/sh stirlingpdfuser && \
# mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
# Set up necessary directories and permissions
RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
# chown -R stirlingpdfuser:stirlingpdfgroup /usr/share/fonts/opentype/noto /configs /customFiles
# Copy necessary files
COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
COPY ./pipeline/ /pipeline/
COPY src/main/resources/static/fonts/*.ttf /usr/share/fonts/opentype/noto/
COPY src/main/resources/static/fonts/*.otf /usr/share/fonts/opentype/noto/
COPY build/libs/*.jar app.jar
# Set font cache and permissions
RUN fc-cache -f -v && \
chmod +x /scripts/init-without-ocr.sh && \
chmod +x /scripts/download-security-jar.sh
# chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Expose the application port
EXPOSE 8080
# Set environment variables
ENV ENDPOINTS_GROUPS_TO_REMOVE=Python,OpenCV,OCRmyPDF
ENV DOCKER_ENABLE_SECURITY=false
# Run the application
#USER stirlingpdfuser
ENTRYPOINT ["/scripts/init-without-ocr.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

View File

@@ -1,5 +1,5 @@
# use alpine # Build jbig2enc in a separate stage
FROM alpine:3.20.0 FROM bellsoft/liberica-openjdk-alpine:17
ARG VERSION_TAG ARG VERSION_TAG
@@ -7,43 +7,40 @@ ARG VERSION_TAG
ENV DOCKER_ENABLE_SECURITY=false \ ENV DOCKER_ENABLE_SECURITY=false \
HOME=/home/stirlingpdfuser \ HOME=/home/stirlingpdfuser \
VERSION_TAG=$VERSION_TAG \ VERSION_TAG=$VERSION_TAG \
JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75" \ JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS -XX:MaxRAMPercentage=75"
PUID=1000 \ # PUID=1000 \
PGID=1000 \ # PGID=1000 \
UMASK=022 # UMASK=022 \
# Copy necessary files # Create user and group using Alpine's addgroup and adduser
COPY scripts/download-security-jar.sh /scripts/download-security-jar.sh #RUN addgroup -g $PGID stirlingpdfgroup && \
COPY scripts/init-without-ocr.sh /scripts/init-without-ocr.sh # adduser -u $PUID -G stirlingpdfgroup -s /bin/sh -D stirlingpdfuser && \
COPY pipeline /pipeline # mkdir -p $HOME && chown stirlingpdfuser:stirlingpdfgroup $HOME
COPY build/libs/*.jar app.jar
# Set up necessary directories and permissions # Set up necessary directories and permissions
RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/main" | tee -a /etc/apk/repositories && \ #RUN mkdir -p /scripts /configs /customFiles && \
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/community" | tee -a /etc/apk/repositories && \ # chown -R stirlingpdfuser:stirlingpdfgroup /scripts /configs /customFiles /logs /pipeline /pipeline/defaultWebUIConfigs /pipeline/watchedFolders /pipeline/finishedFolders
echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" | tee -a /etc/apk/repositories && \
apk upgrade --no-cache -a && \ RUN mkdir -p /scripts /usr/share/fonts/opentype/noto /configs /customFiles
apk add --no-cache \ COPY ./scripts/download-security-jar.sh /scripts/download-security-jar.sh
ca-certificates \ COPY ./scripts/init-without-ocr.sh /scripts/init-without-ocr.sh
tzdata \ COPY ./pipeline/ /pipeline/
tini \ COPY build/libs/*.jar app.jar
bash \
curl \ # Set font cache and permissions
shadow \ #RUN chown stirlingpdfuser:stirlingpdfgroup /app.jar
su-exec \
openjdk21-jre && \ RUN chmod +x /scripts/init-without-ocr.sh && \
# User permissions chmod +x /scripts/download-security-jar.sh && \
mkdir /configs /logs /customFiles && \ apk add --no-cache curl
chmod +x /scripts/*.sh && \
addgroup -S stirlingpdfgroup && adduser -S stirlingpdfuser -G stirlingpdfgroup && \ # Expose the application port
chown -R stirlingpdfuser:stirlingpdfgroup $HOME /scripts /configs /customFiles /pipeline && \ EXPOSE 8080
chown stirlingpdfuser:stirlingpdfgroup /app.jar
# Set environment variables # Set environment variables
ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI ENV ENDPOINTS_GROUPS_TO_REMOVE=CLI
EXPOSE 8080/tcp ENTRYPOINT ["/scripts/init-without-ocr.sh"]
# Run the application # Run the application
ENTRYPOINT ["tini", "--", "/scripts/init-without-ocr.sh"]
CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"] CMD ["java", "-Dfile.encoding=UTF-8", "-jar", "/app.jar"]

50
DockerfileBase Normal file
View File

@@ -0,0 +1,50 @@
# Main stage
FROM ubuntu:latest AS base
# JDK for app
RUN apt-get update && \
apt-get install -y --no-install-recommends \
openjdk-17-jre
# Doc conversion
RUN apt-get update && \
apt-get install -y --no-install-recommends \
libreoffice-core \
libreoffice-common \
libreoffice-writer \
libreoffice-calc \
libreoffice-impress \
python3-uno \
curl \
unoconv
# OCR MY PDF (unpaper for descew and other advanced featues)
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common gnupg2 && \
add-apt-repository ppa:alex-p/tesseract-ocr5 && apt install -y --no-install-recommends tesseract-ocr && \
apt-get update && \
apt-get install -y --no-install-recommends \
ghostscript \
python3-pip \
ocrmypdf \
unpaper && \
pip install --upgrade pip && \
pip install --no-cache-dir --upgrade ocrmypdf && \
pip install --no-cache-dir --upgrade pillow==10.0.1 reportlab==3.6.13 wheel==0.38.1 setuptools==65.5.1 pyjwt==2.4.0 cryptography==39.0.1
#CV and HTML
RUN pip install --no-cache-dir opencv-python-headless WeasyPrint
# cleanup and etc
RUN rm -rf /var/lib/apt/lists/* && \
mkdir /usr/share/tesseract-ocr-original && \
cp -r /usr/share/tesseract-ocr/* /usr/share/tesseract-ocr-original && \
rm -rf /usr/share/tesseract-ocr

View File

@@ -1,46 +1,46 @@
| Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript | | Operation | PageOps | Convert | Security | Other | CLI | Python | OpenCV | LibreOffice | OCRmyPDF | Java | Javascript |
|---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------| |---------------------|---------|---------|----------|-------|------|--------|--------|-------------|----------|----------|------------|
| adjust-contrast | ✔️ | | | | | | | | | | ✔️ | | adjust-contrast | ✔️ | | | | | | | | | | ✔️ |
| auto-split-pdf | ✔️ | | | | | | | | | ✔️ | | | auto-split-pdf | ✔️ | | | | | | | | | ✔️ | |
| crop | ✔️ | | | | | | | | | ✔️ | | | crop | ✔️ | | | | | | | | | ✔️ | |
| extract-page | ✔️ | | | | | | | | | ✔️ | | | extract-page | ✔️ | | | | | | | | | ✔️ | |
| merge-pdfs | ✔️ | | | | | | | | | ✔️ | | | merge-pdfs | ✔️ | | | | | | | | | ✔️ | |
| multi-page-layout | ✔️ | | | | | | | | | ✔️ | | | multi-page-layout | ✔️ | | | | | | | | | ✔️ | |
| pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ | | pdf-organizer | ✔️ | | | | | | | | | ✔️ | ✔️ |
| pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | | | pdf-to-single-page | ✔️ | | | | | | | | | ✔️ | |
| remove-pages | ✔️ | | | | | | | | | ✔️ | | | remove-pages | ✔️ | | | | | | | | | ✔️ | |
| rotate-pdf | ✔️ | | | | | | | | | ✔️ | | | rotate-pdf | ✔️ | | | | | | | | | ✔️ | |
| scale-pages | ✔️ | | | | | | | | | ✔️ | | | scale-pages | ✔️ | | | | | | | | | ✔️ | |
| split-pdfs | ✔️ | | | | | | | | | ✔️ | | | split-pdfs | ✔️ | | | | | | | | | ✔️ | |
| file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | file-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| img-to-pdf | | ✔️ | | | | | | | | ✔️ | | | img-to-pdf | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-html | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-img | | ✔️ | | | | | | | | ✔️ | | | pdf-to-img | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | | | pdf-to-pdfa | | ✔️ | | | ✔️ | | | | ✔️ | | |
| pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | | | pdf-to-markdown | | ✔️ | | | | | | | | ✔️ | |
| pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-presentation | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-text | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-word | | ✔️ | | | ✔️ | | | ✔️ | | | |
| pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | | | pdf-to-xml | | ✔️ | | | ✔️ | | | ✔️ | | | |
| xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | | | xlsx-to-pdf | | ✔️ | | | ✔️ | | | ✔️ | | | |
| add-password | | | ✔️ | | | | | | | ✔️ | | | add-password | | | ✔️ | | | | | | | ✔️ | |
| add-watermark | | | ✔️ | | | | | | | ✔️ | | | add-watermark | | | ✔️ | | | | | | | ✔️ | |
| cert-sign | | | ✔️ | | | | | | | ✔️ | | | cert-sign | | | ✔️ | | | | | | | ✔️ | |
| change-permissions | | | ✔️ | | | | | | | ✔️ | | | change-permissions | | | ✔️ | | | | | | | ✔️ | |
| remove-password | | | ✔️ | | | | | | | ✔️ | | | remove-password | | | ✔️ | | | | | | | ✔️ | |
| sanitize-pdf | | | ✔️ | | | | | | | ✔️ | | | sanitize-pdf | | | ✔️ | | | | | | | ✔️ | |
| add-image | | | | ✔️ | | | | | | ✔️ | | | add-image | | | | ✔️ | | | | | | ✔️ | |
| add-page-numbers | | | | ✔️ | | | | | | ✔️ | | | add-page-numbers | | | | ✔️ | | | | | | ✔️ | |
| auto-rename | | | | ✔️ | | | | | | ✔️ | | | auto-rename | | | | ✔️ | | | | | | ✔️ | |
| change-metadata | | | | ✔️ | | | | | | ✔️ | | | change-metadata | | | | ✔️ | | | | | | ✔️ | |
| compare | | | | ✔️ | | | | | | | ✔️ | | compare | | | | ✔️ | | | | | | | ✔️ |
| compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | compress-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | extract-image-scans | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| extract-images | | | | ✔️ | | | | | | ✔️ | | | extract-images | | | | ✔️ | | | | | | ✔️ | |
| flatten | | | | ✔️ | | | | | | | ✔️ | | flatten | | | | ✔️ | | | | | | | ✔️ |
| get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | | | get-info-on-pdf | | | | ✔️ | | | | | | ✔️ | |
| ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | | | ocr-pdf | | | | ✔️ | ✔️ | | | | ✔️ | | |
| remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | | | remove-blanks | | | | ✔️ | ✔️ | ✔️ | ✔️ | | | | |
| repair | | | | ✔️ | ✔️ | | | ✔️ | | | | | repair | | | | ✔️ | ✔️ | | | ✔️ | | | |
| show-javascript | | | | ✔️ | | | | | | | ✔️ | | show-javascript | | | | ✔️ | | | | | | | ✔️ |
| sign | | | | ✔️ | | | | | | | ✔️ | | sign | | | | ✔️ | | | | | | | ✔️ |

View File

@@ -1,33 +0,0 @@
## User Guide for Local Directory Scanning and File Processing
### Setting Up Watched Folders:
- Create a folder where you want your files to be monitored. This is your 'watched folder'.
- The default directory for this is `./pipeline/watchedFolders/`
- Place any directories you want to be scanned into this folder, this folder should contain multiple folders each for their own tasks and pipelines.
### Configuring Processing with JSON Files:
- In each directory you want processed (e.g `./pipeline/watchedFolders/officePrinter`), include a JSON configuration file.
- This JSON file should specify how you want the files in the directory to be handled (e.g., what operations to perform on them) which can be made, configured and downloaded from Stirling-PDF Pipeline interface.r
### Automatic Scanning and Processing:
- The system automatically checks the watched folder every minute for new directories and files to process.
- When a directory with a valid JSON configuration file is found, it begins processing the files inside as per the configuration.
### Processing Steps:
- Files in each directory are processed according to the instructions in the JSON file.
- This might involve file conversions, data filtering, renaming files, etc. If the output of a step is a zip, this zip will be automatically unzipped as it passes to next process.
### Results and Output:
- After processing, the results are saved in a specified output location. This could be a different folder or location as defined in the JSON file or the default location `./pipeline/finishedFolders/`.
- Each processed file is named and organized according to the rules set in the JSON configuration.
### Completion and Cleanup:
- Once processing is complete, the original files in the watched folder's directory are removed.
- You can find the processed files in the designated output location.
### Error Handling:
- If there's an error during processing, the system will not delete the original files, allowing you to check and retry if necessary.
### User Interaction:
- As a user, your main tasks are to set up the watched folders, place directories with files for processing, and create the corresponding JSON configuration files.
- The system handles the rest, including scanning, processing, and outputting results.

View File

@@ -1,4 +1,4 @@
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1> <p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
</p> </p>
@@ -8,44 +8,31 @@ Fork Stirling-PDF and make a new branch out of Main
Then add reference to the language in the navbar by adding a new language entry to the dropdown Then add reference to the language in the navbar by adding a new language entry to the dropdown
https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/templates/fragments/languages.html
and add a flag svg file to and add a flag svg file to
https://github.com/Stirling-Tools/Stirling-PDF/tree/main/src/main/resources/static/images/flags https://github.com/Frooodle/Stirling-PDF/tree/main/src/main/resources/static/images/flags
Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/) Any SVG flags are fine, i got most of mine from [here](https://flagicons.lipis.dev/)
If your language isn't represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia If your language isnt represented by a flag just find whichever closely matches it, such as for Arabic i chose Saudi Arabia
For example to add Polish you would add For example to add Polish you would add
```html ```
<a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL"> <a class="dropdown-item lang_dropdown-item" href="" data-language-code="pl_PL">
<img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski <img src="images/flags/pl.svg" alt="icon" width="20" height="15"> Polski
</a> </a>
``` ```
The data-language-code is the code used to reference the file in the next step. The data-language-code is the code used to reference the file in the next step.
Start by copying the existing english property file Start by copying the existing english property file
[https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties) [https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties](https://github.com/Frooodle/Stirling-PDF/blob/main/src/main/resources/messages_en_GB.properties)
Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties Copy and rename it to messages_{your data-language-code here}.properties, in the polish example you would set the name to messages_pl_PL.properties
Then simply translate all property entries within that file and make a PR into main for others to use! Then simply translate all property entries within that file and make a PR into main for others to use!
If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but won't be able to verify the translations themselves) If you do not have a java IDE i am happy to verify the changes worked once you raise PR (but wont be able to verify the translations themselves)
## Handling Untranslatable Strings
Sometimes, certain strings in the properties file may not require translation because they are the same in the target language or are universal (like names of protocols, certain terminologies, etc.). To ensure accurate statistics for language progress, these strings should be added to the `ignore_translation.toml` file located in the `scripts` directory. This will exclude them from the translation progress calculations.
For example, if the English string error=Error does not need translation in Polish, add it to the ignore_translation.toml under the Polish section:
```toml
[pl_PL]
ignore = [
"language.direction", # Existing entries
"error" # Add new entries here
]
```
Make sure to place the entry under the correct language section. This helps maintain the accuracy of translation progress statistics and ensures that the translation tool or scripts do not misinterpret the completion rate.

View File

@@ -2,12 +2,12 @@
This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker. This document provides instructions on how to add additional language packs for the OCR tab in Stirling-PDF, both inside and outside of Docker.
## My OCR used to work and now doesn't! ## My OCR used to work and now doesnt!
The paths have changed for the tessadata locations on new docker images, please use ``/usr/share/tessdata`` (Others should still work for backwards compatibility but might not) Please update your tesseract docker volume path version from 4.00 to 5
## How does the OCR Work ## How does the OCR Work
Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition. Stirling-PDF uses [OCRmyPDF](https://github.com/ocrmypdf/OCRmyPDF) which in turn uses tesseract for its text recognition.
All credit goes to them for this awesome work! All credit goes to them for this awesome work!
## Language Packs ## Language Packs
@@ -21,13 +21,13 @@ Depending on your requirements, you can choose the appropriate language pack for
### Installing Language Packs ### Installing Language Packs
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need. 1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata` 2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/5/tessdata` (Debian) or `/usr/share/tesseract/tessdata` (Fedora)
# DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED. # DO NOT REMOVE EXISTING ENG.TRAINEDDATA, IT'S REQUIRED.
#### Docker #### Docker
If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs. If you are using Docker, you need to expose the Tesseract tessdata directory as a volume in order to use the additional language packs.
#### Docker Compose #### Docker Compose
Modify your `docker-compose.yml` file to include the following volume configuration: Modify your `docker-compose.yml` file to include the following volume configuration:
@@ -37,14 +37,14 @@ services:
your_service_name: your_service_name:
image: your_docker_image_name image: your_docker_image_name
volumes: volumes:
- /location/of/trainingData:/usr/share/tessdata - /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata
``` ```
#### Docker run #### Docker run
Add the following to your existing docker run command Add the following to your existing docker run command
```bash ```bash
-v /location/of/trainingData:/usr/share/tessdata -v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata
``` ```
#### Non-Docker #### Non-Docker

88
Jenkinsfile vendored
View File

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

View File

@@ -14,7 +14,7 @@ You could theoretically use a Distrobox/Toolbox, if your Distribution has old or
Install the following software, if not already installed: Install the following software, if not already installed:
- Java 17 or later (21 recommended) - Java 17 or later
- Gradle 7.0 or later (included within repo so not needed on server) - Gradle 7.0 or later (included within repo so not needed on server)
@@ -42,25 +42,17 @@ For Debian-based systems, you can use the following command:
```bash ```bash
sudo apt-get update sudo apt-get update
sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ openjdk-21-jdk python3 python3-pip sudo apt-get install -y git automake autoconf libtool libleptonica-dev pkg-config zlib1g-dev make g++ java-17-openjdk python3 python3-pip
``` ```
For Fedora-based systems use this command: For Fedora-based systems use this command:
```bash ```bash
sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-21-openjdk python3 python3-pip sudo dnf install -y git automake autoconf libtool leptonica-devel pkg-config zlib-devel make gcc-c++ java-17-openjdk python3 python3-pip
```
For non-root users with Nix Package Manager, use the following command:
```bash
nix-channel --update
nix-env -iA nixpkgs.jdk21 nixpkgs.git nixpkgs.python38 nixpkgs.gnumake nixpkgs.libgcc nixpkgs.automake nixpkgs.autoconf nixpkgs.libtool nixpkgs.pkg-config nixpkgs.zlib nixpkgs.leptonica
``` ```
### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality) ### Step 2: Clone and Build jbig2enc (Only required for certain OCR functionality)
For Debian and Fedora, you can build it from source using the following commands:
```bash ```bash
mkdir ~/.git mkdir ~/.git
cd ~/.git &&\ cd ~/.git &&\
@@ -72,13 +64,8 @@ make &&\
sudo make install sudo make install
``` ```
For Nix, you will face `Leptonica not detected`. Bypass this by installing it directly using the following command:
```bash
nix-env -iA nixpkgs.jbig2enc
```
### Step 3: Install Additional Software ### Step 3: Install Additional Software
Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for pattern recognition functionality. Next we need to install LibreOffice for conversions, ocrmypdf for OCR, and opencv for patern recognition functionality.
Install the following software: Install the following software:
@@ -108,39 +95,33 @@ For Debian-based systems, you can use the following command:
```bash ```bash
sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf sudo apt-get install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint --break-system-packages pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
``` ```
For Fedora: For Fedora:
```bash ```bash
sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf sudo dnf install -y libreoffice-writer libreoffice-calc libreoffice-impress unpaper ocrmypdf
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
```
For Nix:
```bash
nix-env -iA nixpkgs.unpaper nixpkgs.libreoffice nixpkgs.ocrmypdf nixpkgs.poppler_utils
pip3 install uno opencv-python-headless unoconv pngquant WeasyPrint
``` ```
### Step 4: Clone and Build Stirling-PDF ### Step 4: Clone and Build Stirling-PDF
```bash ```bash
cd ~/.git &&\ cd ~/.git &&\
git clone https://github.com/Stirling-Tools/Stirling-PDF.git &&\ git clone https://github.com/Frooodle/Stirling-PDF.git &&\
cd Stirling-PDF &&\ cd Stirling-PDF &&\
chmod +x ./gradlew &&\ chmod +x ./gradlew &&\
./gradlew build ./gradlew build
``` ```
### Step 5: Move jar to desired location ### Step 5: Move jar to desired location
After the build process, a `.jar` file will be generated in the `build/libs` directory. After the build process, a `.jar` file will be generated in the `build/libs` directory.
You can move this file to a desired location, for example, `/opt/Stirling-PDF/`. You can move this file to a desired location, for example, `/opt/Stirling-PDF/`.
You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory. You must also move the Script folder within the Stirling-PDF repo that you have downloaded to this directory.
This folder is required for the python scripts using OpenCV. This folder is required for the python scripts using OpenCV
```bash ```bash
sudo mkdir /opt/Stirling-PDF &&\ sudo mkdir /opt/Stirling-PDF &&\
@@ -148,25 +129,19 @@ sudo mv ./build/libs/Stirling-PDF-*.jar /opt/Stirling-PDF/ &&\
sudo mv scripts /opt/Stirling-PDF/ &&\ sudo mv scripts /opt/Stirling-PDF/ &&\
echo "Scripts installed." echo "Scripts installed."
``` ```
For non-root users, you can just keep the jar in the main directory of Stirling-PDF using the following command:
```bash
mv ./build/libs/Stirling-PDF-*.jar ./Stirling-PDF-*.jar
```
### Step 6: Other files ### Step 6: Other files
#### OCR #### OCR
If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning. If you plan to use the OCR (Optical Character Recognition) functionality, you might need to install language packs for Tesseract if running non-english scanning.
##### Installing Language Packs ##### Installing Language Packs
Easiest is to use the langpacks provided by your repositories. Skip the other steps. Easiest is to use the langpacks provided by your repositories. Skip the other steps
Manual: Manual:
1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need. 1. Download the desired language pack(s) by selecting the `.traineddata` file(s) for the language(s) you need.
2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tessdata` 2. Place the `.traineddata` files in the Tesseract tessdata directory: `/usr/share/tesseract-ocr/5/tessdata`
3. Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info. 3.
Please view [OCRmyPDF install guide](https://ocrmypdf.readthedocs.io/en/latest/installation.html) for more info.
**IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED. **IMPORTANT:** DO NOT REMOVE EXISTING `eng.traineddata`, IT'S REQUIRED.
Debian based systems, install languages with this command: Debian based systems, install languages with this command:
@@ -196,38 +171,14 @@ dnf search -C tesseract-langpack-
rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g' rpm -qa | grep tesseract-langpack | sed 's/tesseract-langpack-//g'
``` ```
Nix:
```bash
nix-env -iA nixpkgs.tesseract
```
**Note:** Nix Package Manager pre-installs almost all the language packs when tesseract is installed.
### Step 7: Run Stirling-PDF ### Step 7: Run Stirling-PDF
Those who have pushed to the root directory, run the following commands:
```bash ```bash
./gradlew bootRun ./gradlew bootRun
or or
java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar java -jar /opt/Stirling-PDF/Stirling-PDF-*.jar
``` ```
Since libreoffice, soffice, and conversion tools have their dbus_tmp_dir set as `dbus_tmp_dir="/run/user/$(id -u)/libreoffice-dbus"`, you might get the following error when using their endpoints:
```
[Thread-7] INFO s.s.SPDF.utils.ProcessExecutor - mkdir: cannot create directory /run/user/1501: Permission denied
```
To resolve this, before starting the Stirling-PDF, you have to set the environment variable to a directory you have write access to by using the following commands:
```bash
mkdir temp
export DBUS_SESSION_BUS_ADDRESS="unix:path=./temp"
./gradlew bootRun
or
java -jar ./Stirling-PDF-*.jar
```
### Step 8: Adding a Desktop icon ### Step 8: Adding a Desktop icon
This will add a modified Appstarter to your Appmenu. This will add a modified Appstarter to your Appmenu.
@@ -251,19 +202,7 @@ EOF
Note: Currently the app will run in the background until manually closed. Note: Currently the app will run in the background until manually closed.
### Optional: Changing the host and port of the application: ### Optional: Run Stirling-PDF as a service
To override the default configuration, you can add the following to `/.git/Stirling-PDF/configs/custom_settings.yml` file:
```bash
server:
host: 0.0.0.0
port: 3000
```
**Note:** This file is created after the first application launch. To have it before that, you can create the directory and add the file yourself.
### Optional: Run Stirling-PDF as a service (requires root).
First create a .env file, where you can store environment variables: First create a .env file, where you can store environment variables:
``` ```
@@ -300,7 +239,6 @@ WantedBy=multi-user.target
``` ```
Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file): Notify systemd that it has to rebuild its internal service database (you have to run this command every time you make a change in the service file):
``` ```
sudo systemctl daemon-reload sudo systemctl daemon-reload
``` ```
@@ -326,10 +264,10 @@ sudo systemctl restart stirlingpdf.service
Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme. Remember to set the necessary environment variables before running the project if you want to customize the application the list can be seen in the main readme.
You can do this in the terminal by using the `export` command or -D argument to java -jar command: You can do this in the terminal by using the `export` command or -D arguements to java -jar command:
```bash ```bash
export APP_HOME_NAME="Stirling PDF" export APP_HOME_NAME="Stirling PDF"
or or
-DAPP_HOME_NAME="Stirling PDF" -DAPP_HOME_NAME="Stirling PDF"
``` ```

View File

@@ -1,44 +0,0 @@
# Pipeline Configuration and Usage Tutorial
- Configure the pipeline config file and input files to run files against it
- For reuse, download the config file and re-upload it when needed, or place it in /pipeline/defaultWebUIConfigs/ to auto-load in the web UI for all users
## Steps to Configure and Use Your Pipeline
1. **Access Configuration**
- Upon entering the screen, click on the **Configure** button.
2. **Enter Pipeline Name**
- Provide a name for your pipeline in the designated field.
3. **Select Operations**
- Choose the operations for your pipeline (e.g., **Split Pages**), then click **Add Operation**.
4. **Configure Operation Settings**
- Input the necessary settings for each added operation. Settings are highlighted in yellow if customization is needed.
5. **Add More Operations**
- You can add and adjust the order of multiple operations. Ensure each operation's settings are customized.
6. **Save Settings**
- Click **Save Operation Settings** after customizing settings for each operation.
7. **Validate Pipeline**
- Use the **Validation** button to check your pipeline. A green indicator signifies correct setup; a pop-out error indicates issues.
8. **Download Pipeline Configuration**
- To use the configuration for folder scanning (or save it for future use and reupload it), you can also download a JSON file in this menu. You can also pre-load this for future use by placing it in ``/pipeline/defaultWebUIConfigs/``. It will then appear in the dropdown menu for all users to use.
9. **Submit Files for Processing**
- If your pipeline is correctly set up close the configure menu, input the files and hit **Submit**.
10. **Note on Web UI Limitations**
- The current web UI version does not support operations that require multiple different types of inputs, such as adding a separate image to a PDF.
### Current Limitations
- Cannot have more than one of the same operation
- Cannot input additional files via UI
- All files and operations run in serial mode

293
README.md
View File

@@ -1,139 +1,131 @@
<p align="center"><img src="https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/main/docs/stirling.png" width="80" ></p> <p align="center"><img src="https://raw.githubusercontent.com/Frooodle/Stirling-PDF/main/docs/stirling.png" width="80" ><br><h1 align="center">Stirling-PDF</h1>
<h1 align="center">Stirling-PDF</h1> </p>
[![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf) [![Docker Pulls](https://img.shields.io/docker/pulls/frooodle/s-pdf)](https://hub.docker.com/r/frooodle/s-pdf)
[![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ) [![Discord](https://img.shields.io/discord/1068636748814483718?label=Discord)](https://discord.gg/Cn8pWhQRxZ)
[![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Stirling-Tools/Stirling-PDF/) [![Docker Image Version (tag latest semver)](https://img.shields.io/docker/v/frooodle/s-pdf/latest)](https://github.com/Frooodle/Stirling-PDF/)
[![GitHub Repo stars](https://img.shields.io/github/stars/stirling-tools/stirling-pdf?style=social)](https://github.com/Stirling-Tools/stirling-pdf) [![GitHub Repo stars](https://img.shields.io/github/stars/frooodle/stirling-pdf?style=social)](https://github.com/Frooodle/stirling-pdf)
[![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/donate/?hosted_button_id=MN7JPG5G6G3JL) [![Paypal Donate](https://img.shields.io/badge/Paypal%20Donate-yellow?style=flat&logo=paypal)](https://www.paypal.com/paypalme/froodleplex)
[![Github Sponsor](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle) [![Github Sponser](https://img.shields.io/badge/Github%20Sponsor-yellow?style=flat&logo=github)](https://github.com/sponsors/Frooodle)
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Stirling-Tools/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af) [![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/Frooodle/Stirling-PDF/tree/digitalOcean&refcode=c3210994b1af)
This is a robust, locally hosted web-based PDF manipulation tool using Docker. It enables you to carry out various operations on PDF files, including splitting, merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application has evolved to encompass a comprehensive set of features, addressing all your PDF requirements. This is a powerful locally hosted web based PDF manipulation tool using docker that allows you to perform various operations on PDF files, such as splitting merging, converting, reorganizing, adding images, rotating, compressing, and more. This locally hosted web application started as a 100% ChatGPT-made application and has evolved to include a wide range of features to handle all your PDF needs.
Stirling PDF does not initiate any outbound calls for record-keeping or tracking purposes. Stirling PDF makes no outbound calls for any record keeping or tracking.
All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point. All files and PDFs exist either exclusively on the client side, reside in server memory only during task execution, or temporarily reside in a file solely for the execution of the task. Any file downloaded by the user will have been deleted from the server by that point.
![stirling-home](images/stirling-home.jpg) Please feel free to submit feature requests or report bugs either through GitHub issues or on our [Discord](https://discord.gg/Cn8pWhQRxZ)
![stirling-home](images/stirling-home.png)
## Features ## Features
- Dark mode support. - Dark mode support.
- Custom download options (see [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/images/settings.png) for example) - Custom download options (see [here](https://github.com/Frooodle/Stirling-PDF/blob/main/images/settings.png) for example)
- Parallel file processing and downloads - Parallel file processing and downloads
- API for integration with external scripts - API for integration with external scripts
- Optional Login and Authentication support (see [here](https://github.com/Stirling-Tools/Stirling-PDF/tree/main#login-authentication) for documentation) - Optional Login and Authentication support (see [here](https://github.com/Frooodle/Stirling-PDF/tree/main#login-authentication) for documentation)
## **PDF Features** ## **PDF Features**
### **Page Operations** ### **Page Operations**
- View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts) - View and modify PDFs - View multi page PDFs with custom viewing sorting and searching. Plus on page edit features like annotate, draw and adding text and images. (Using PDF.js with Joxit and Liberation.Liberation fonts)
- Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages. - Full interactive GUI for merging/splitting/rotating/moving PDFs and their pages.
- Merge multiple PDFs together into a single resultant file. - Merge multiple PDFs together into a single resultant file.
- Split PDFs into multiple files at specified page numbers or extract all pages as individual files. - Split PDFs into multiple files at specified page numbers or extract all pages as individual files.
- Reorganize PDF pages into different orders. - Reorganize PDF pages into different orders.
- Rotate PDFs in 90-degree increments. - Rotate PDFs in 90-degree increments.
- Remove pages. - Remove pages.
- Multi-page layout (Format PDFs into a multi-paged page). - Multi-page layout (Format PDFs into a multi-paged page).
- Scale page contents size by set %. - Scale page contents size by set %.
- Adjust Contrast. - Adjust Contrast.
- Crop PDF. - Crop PDF.
- Auto Split PDF (With physically scanned page dividers). - Auto Split PDF (With physically scanned page dividers).
- Extract page(s). - Extract page(s).
- Convert PDF to a single page. - Convert PDF to a single page.
### **Conversion Operations** ### **Conversion Operations**
- Convert PDFs to and from images.
- Convert PDFs to and from images. - Convert any common file to PDF (using LibreOffice).
- Convert any common file to PDF (using LibreOffice). - Convert PDF to Word/Powerpoint/Others (using LibreOffice).
- Convert PDF to Word/Powerpoint/Others (using LibreOffice). - Convert HTML to PDF.
- Convert HTML to PDF. - URL to PDF.
- URL to PDF. - Markdown to PDF.
- Markdown to PDF.
### **Security & Permissions** ### **Security & Permissions**
- Add and remove passwords.
- Add and remove passwords. - Change/set PDF Permissions.
- Change/set PDF Permissions. - Add watermark(s).
- Add watermark(s). - Certify/sign PDFs.
- Certify/sign PDFs. - Sanitize PDFs.
- Sanitize PDFs. - Auto-redact text.
- Auto-redact text.
### **Other Operations** ### **Other Operations**
- Add/Generate/Write signatures.
- Repair PDFs.
- Detect and remove blank pages.
- Compare 2 PDFs and show differences in text.
- Add images to PDFs.
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
- Extract images from PDF.
- Extract images from Scans.
- Add page numbers.
- Auto rename file by detecting PDF header text.
- OCR on PDF (Using OCRMyPDF).
- PDF/A conversion (Using OCRMyPDF).
- Edit metadata.
- Flatten PDFs.
- Get all information on a PDF to view or export as JSON.
- Add/Generate/Write signatures.
- Repair PDFs.
- Detect and remove blank pages.
- Compare 2 PDFs and show differences in text.
- Add images to PDFs.
- Compress PDFs to decrease their filesize (Using OCRMyPDF).
- Extract images from PDF.
- Extract images from Scans.
- Add page numbers.
- Auto rename file by detecting PDF header text.
- OCR on PDF (Using OCRMyPDF).
- PDF/A conversion (Using OCRMyPDF).
- Edit metadata.
- Flatten PDFs.
- Get all information on a PDF to view or export as JSON.
For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md) For a overview of the tasks and the technology each uses please view [Endpoint-groups.md](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
Demo of the app is available [here](https://stirlingpdf.io). username: demo, password: demo Hosted instance/demo of the app can be seen [here](https://pdf.adminforge.de/) hosted by the team at adminforge.de
## Technologies used ## Technologies used
- Spring Boot + Thymeleaf - Spring Boot + Thymeleaf
- [PDFBox](https://github.com/apache/pdfbox/tree/trunk) - PDFBox
- [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions - [LibreOffice](https://www.libreoffice.org/discover/libreoffice/) for advanced conversions
- [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF) - [OcrMyPdf](https://github.com/ocrmypdf/OCRmyPDF)
- HTML, CSS, JavaScript - HTML, CSS, JavaScript
- Docker - Docker
- [PDF.js](https://github.com/mozilla/pdf.js) - PDF.js
- [PDF-LIB.js](https://github.com/Hopding/pdf-lib) - PDF-LIB.js
## How to use ## How to use
### Locally ### Locally
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/LocalRunGuide.md
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/LocalRunGuide.md
### Docker / Podman ### Docker / Podman
https://hub.docker.com/r/frooodle/s-pdf https://hub.docker.com/r/frooodle/s-pdf
Stirling PDF has 2 different versions, a Full version and ultra-Lite version. Depending on the types of features you use you may want a smaller image to save on space. Stirling PDF has 3 different versions, a Full version, Lite, and ultra-Lite. 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) To see what the different versions offer please look at our [version mapping](https://github.com/Frooodle/Stirling-PDF/blob/main/Version-groups.md)
For people that don't mind about space optimization just use the latest tag. For people that don't mind about space optimization just use the latest tag.
![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/frooodle/s-pdf/latest?label=Stirling-PDF%20Full)
![Docker Image Size (tag)](https://img.shields.io/docker/image-size/frooodle/s-pdf/latest-lite?label=Stirling-PDF%20Lite)
![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/frooodle/s-pdf/latest-ultra-lite?label=Stirling-PDF%20Ultra-Lite)
Docker Run Docker Run
```
```bash
docker run -d \ docker run -d \
-p 8080:8080 \ -p 8080:8080 \
-v /location/of/trainingData:/usr/share/tessdata \ -v /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata \
-v /location/of/extraConfigs:/configs \ -v /location/of/extraConfigs:/configs \
-v /location/of/logs:/logs \ -v /location/of/logs:/logs \
-e DOCKER_ENABLE_SECURITY=false \ -e DOCKER_ENABLE_SECURITY=false \
-e INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false \
-e LANGS=en_GB \
--name stirling-pdf \ --name stirling-pdf \
frooodle/s-pdf:latest frooodle/s-pdf:latest
Can also add these for customisation but are not required Can also add these for customisation but are not required
-v /location/of/customFiles:/customFiles \ -v /location/of/customFiles:/customFiles \
``` ```
Docker Compose Docker Compose
```
```yaml
version: '3.3' version: '3.3'
services: services:
stirling-pdf: stirling-pdf:
@@ -141,76 +133,67 @@ services:
ports: ports:
- '8080:8080' - '8080:8080'
volumes: volumes:
- /location/of/trainingData:/usr/share/tessdata #Required for extra OCR languages - /location/of/trainingData:/usr/share/tesseract-ocr/5/tessdata #Required for extra OCR languages
- /location/of/extraConfigs:/configs - /location/of/extraConfigs:/configs
# - /location/of/customFiles:/customFiles/ # - /location/of/customFiles:/customFiles/
# - /location/of/logs:/logs/ # - /location/of/logs:/logs/
environment: environment:
- DOCKER_ENABLE_SECURITY=false - DOCKER_ENABLE_SECURITY=false
- INSTALL_BOOK_AND_ADVANCED_HTML_OPS=false
- LANGS=en_GB
``` ```
Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman". Note: Podman is CLI-compatible with Docker, so simply replace "docker" with "podman".
## Enable OCR/Compression feature ## Enable OCR/Compression feature
Please view https://github.com/Frooodle/Stirling-PDF/blob/main/HowToUseOCR.md
Please view https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToUseOCR.md ## Want to add your own language?
Stirling PDF currently supports 21!
- English (English) (en_GB)
- English (US) (en_US)
- Arabic (العربية) (ar_AR)
- German (Deutsch) (de_DE)
- French (Français) (fr_FR)
- Spanish (Español) (es_ES)
- Chinese (简体中文) (zh_CN)
- Catalan (Català) (ca_CA)
- Italian (Italiano) (it_IT)
- Swedish (Svenska) (sv_SE)
- Polish (Polski) (pl_PL)
- Romanian (Română) (ro_RO)
- Korean (한국어) (ko_KR)
- Portuguese Brazilian (Português) (pt_BR)
- Russian (Русский) (ru_RU)
- Basque (Euskara) (eu_ES)
- Japanese (日本語) (ja_JP)
- Dutch (Nederlands) (nl_NL)
- Greek (el_GR)
- Turkish (Türkçe) (tr_TR)
- Indonesia (Bahasa Indonesia) (id_ID)
- Hindi (हिंदी) (hi_IN)
## Supported Languages If you want to add your own language to Stirling-PDF please refer
https://github.com/Frooodle/Stirling-PDF/blob/main/HowToAddNewLanguage.md
Stirling PDF currently supports 27! And please create a PR to merge it back in so others can use it!
| Language | Progress | ## How to View
| ------------------------------------------- | -------------------------------------- | 1. Open a web browser and navigate to `http://localhost:8080/`
| English (English) (en_GB) | ![100%](https://geps.dev/progress/100) | 2. Use the application by following the instructions on the website.
| English (US) (en_US) | ![100%](https://geps.dev/progress/100) |
| Arabic (العربية) (ar_AR) | ![41%](https://geps.dev/progress/41) |
| German (Deutsch) (de_DE) | ![97%](https://geps.dev/progress/97) |
| French (Français) (fr_FR) | ![94%](https://geps.dev/progress/94) |
| Spanish (Español) (es_ES) | ![97%](https://geps.dev/progress/97) |
| Simplified Chinese (简体中文) (zh_CN) | ![96%](https://geps.dev/progress/96) |
| Traditional Chinese (繁體中文) (zh_TW) | ![96%](https://geps.dev/progress/96) |
| Catalan (Català) (ca_CA) | ![50%](https://geps.dev/progress/50) |
| Italian (Italiano) (it_IT) | ![99%](https://geps.dev/progress/99) |
| Swedish (Svenska) (sv_SE) | ![41%](https://geps.dev/progress/41) |
| Polish (Polski) (pl_PL) | ![43%](https://geps.dev/progress/43) |
| Romanian (Română) (ro_RO) | ![40%](https://geps.dev/progress/40) |
| Korean (한국어) (ko_KR) | ![89%](https://geps.dev/progress/89) |
| Portuguese Brazilian (Português) (pt_BR) | ![62%](https://geps.dev/progress/62) |
| Russian (Русский) (ru_RU) | ![89%](https://geps.dev/progress/89) |
| Basque (Euskara) (eu_ES) | ![65%](https://geps.dev/progress/65) |
| Japanese (日本語) (ja_JP) | ![89%](https://geps.dev/progress/89) |
| Dutch (Nederlands) (nl_NL) | ![86%](https://geps.dev/progress/86) |
| Greek (Ελληνικά) (el_GR) | ![87%](https://geps.dev/progress/87) |
| Turkish (Türkçe) (tr_TR) | ![99%](https://geps.dev/progress/99) |
| Indonesia (Bahasa Indonesia) (id_ID) | ![80%](https://geps.dev/progress/80) |
| Hindi (हिंदी) (hi_IN) | ![81%](https://geps.dev/progress/81) |
| Hungarian (Magyar) (hu_HU) | ![79%](https://geps.dev/progress/79) |
| Bulgarian (Български) (bg_BG) | ![96%](https://geps.dev/progress/96) |
| Sebian Latin alphabet (Srpski) (sr_LATN_RS) | ![82%](https://geps.dev/progress/82) |
| Ukrainian (Українська) (uk_UA) | ![88%](https://geps.dev/progress/88) |
| Slovakian (Slovensky) (sk_SK) | ![96%](https://geps.dev/progress/96) |
## Contributing (creating issues, translations, fixing bugs, etc.)
Please see our [Contributing Guide](CONTRIBUTING.md)!
## Customisation ## Customisation
Stirling PDF allows easy customization of the app. Stirling PDF allows easy customization of the app.
Includes things like Includes things like
- Custom application name - Custom application name
- Custom slogans, icons, HTML, images CSS etc (via file overrides) - Custom slogans, icons, images, and even custom HTML (via file overrides)
There are two options for this, either using the generated settings file ``settings.yml`` There are two options for this, either using the generated settings file ``settings.yml``
This file is located in the ``/configs`` directory and follows standard YAML formatting This file is located in the ``/configs`` directory and follows standard YAML formatting
Environment variables are also supported and would override the settings file Environment variables are also supported and would override the settings file
For example in the settings.yml you have For example in the settings.yml you have
```
```yaml
system: system:
defaultLocale: 'en-US' defaultLocale: 'en-US'
``` ```
@@ -218,75 +201,47 @@ system:
To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE`` To have this via an environment variable you would have ``SYSTEM_DEFAULTLOCALE``
The Current list of settings is The Current list of settings is
```
```yaml
security: security:
enableLogin: false # set to 'true' to enable login enableLogin: false # set to 'true' to enable login
csrfDisabled: true # Set to 'true' to disable CSRF protection (not recommended for production) csrfDisabled: true
loginAttemptCount: 5 # lock user account after 5 tries
loginResetTimeMinutes: 120 # lock account for 2 hours after x attempts
# initialLogin:
# username: "admin" # Initial username for the first login (these are defaulted)
# password: "stirling" # Initial password for the first login
# oauth2:
# enabled: false # set to 'true' to enable login (Note: enableLogin must also be 'true' for this to work)
# issuer: "" # set to any provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
# clientId: "" # Client ID from your provider
# clientSecret: "" # Client Secret from your provider
# autoCreateUser: false # set to 'true' to allow auto-creation of non-existing users
# useAsUsername: "email" # Default is 'email'; custom fields can be used as the username
# scopes: "openid, profile, email" # Specify the scopes for which the application will request permissions
# provider: "google" # Set this to your OAuth provider's name, e.g., 'google' or 'keycloak'
system: system:
defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc) defaultLocale: 'en-US' # Set the default language (e.g. 'de-DE', 'fr-FR', etc)
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
enableAlphaFunctionality: false # Set to enable functionality which might need more testing before it fully goes live (This feature might make no changes) customStaticFilePath: '/customFiles/static/' # Directory path for custom static files
showUpdate: true # see when a new update is available
showUpdateOnlyAdmin: false # Only admins can see when a new update is available, depending on showUpdate it must be set to 'true'
customHTMLFiles: false # enable to have files placed in /customFiles/templates override the existing template html files
ui: #ui:
appName: null # Application's visible name # appName: exampleAppName # Application's visible name
homeDescription: null # Short description or tagline shown on homepage. # homeDescription: I am a description # Short description or tagline shown on homepage.
appNameNavbar: null # Name displayed on the navigation bar # appNameNavbar: navbarName # Name displayed on the navigation bar
endpoints: endpoints:
toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages']) toRemove: [] # List endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice']) groupsToRemove: [] # List groups to disable (e.g. ['LibreOffice'])
metrics: metrics:
enabled: true # 'true' to enable Info APIs (`/api/*`) endpoints, 'false' to disable enabled: true # 'true' to enable Info APIs endpoints (view http://localhost:8080/swagger-ui/index.html#/API to learn more), 'false' to disable
``` ```
There is an additional config file ``/configs/custom_settings.yml`` were users familiar with java and spring application.properties can input their own settings on-top of Stirling-PDFs existing ones
### Extra notes ### Extra notes
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Frooodle/Stirling-PDF/blob/main/Endpoint-groups.md)
- Endpoints. Currently, the endpoints ENDPOINTS_TO_REMOVE and GROUPS_TO_REMOVE can include comma separate lists of endpoints and groups to disable as example ENDPOINTS_TO_REMOVE=img-to-pdf,remove-pages would disable both image-to-pdf and remove pages, GROUPS_TO_REMOVE=LibreOffice Would disable all things that use LibreOffice. You can see a list of all endpoints and groups [here](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/Endpoint-groups.md)
- customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF - customStaticFilePath. Customise static files such as the app logo by placing files in the /customFiles/static/ directory. An example of customising app logo is placing a /customFiles/static/favicon.svg to override current SVG. This can be used to change any images/icons/css/fonts/js etc in Stirling-PDF
### Environment only parameters ### Environment only parameters
- ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app`` - ``SYSTEM_ROOTURIPATH`` ie set to ``/pdf-app`` to Set the application's root URI to ``localhost:8080/pdf-app``
- ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values - ``SYSTEM_CONNECTIONTIMEOUTMINUTES`` to set custom connection timeout values
- ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login) - ``DOCKER_ENABLE_SECURITY`` to tell docker to download security jar (required as true for auth login)
- ``INSTALL_BOOK_AND_ADVANCED_HTML_OPS`` to download calibre onto stirling-pdf enabling pdf to/from book and advanced html conversion
- ``LANGS`` to define custom font libraries to install for use for document conversions
## API ## API
For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation For those wanting to use Stirling-PDFs backend API to link with their own custom scripting to edit PDFs you can view all existing API documentation
[here](https://app.swaggerhub.com/apis-docs/Stirling-Tools/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF) [here](https://app.swaggerhub.com/apis-docs/Frooodle/Stirling-PDF/) or navigate to /swagger-ui/index.html of your stirling-pdf instance for your versions documentation (Or by following the API button in your settings of Stirling-PDF)
## Login authentication ## Login authentication
![stirling-login](images/login-light.png) ![stirling-login](images/login-light.png)
### Prerequisites:
### Prerequisites:
- User must have the folder ./configs volumed within docker so that it is retained during updates. - User must have the folder ./configs volumed within docker so that it is retained during updates.
- Docker users must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables. - Docker uses must download the security jar version by setting ``DOCKER_ENABLE_SECURITY`` to ``true`` in environment variables.
- Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true`` - Then either enable login via the settings.yml file or via setting ``SECURITY_ENABLE_LOGIN`` to ``true``
- Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation). - Now the initial user will be generated with username ``admin`` and password ``stirling``. On login you will be forced to change the password to a new one. You can also use the environment variables ``SECURITY_INITIALLOGIN_USERNAME`` and ``SECURITY_INITIALLOGIN_PASSWORD`` to set your own straight away (Recommended to remove them after user creation).
@@ -300,22 +255,20 @@ To add new users go to the bottom of Account settings and hit 'Admin Settings',
For API usage you must provide a header with 'X-API-Key' and the associated API key for that user. For API usage you must provide a header with 'X-API-Key' and the associated API key for that user.
## FAQ ## FAQ
### Q1: What are your planned features? ### Q1: What are your planned features?
- Progress bar/Tracking - Progress bar/Tracking
- Full custom logic pipelines to combine multiple operations together. - Full custom logic pipelines to combine multiple operations together.
- Folder support with auto scanning to perform operations on - Folder support with auto scanning to perform operations on
- Redact text (Via UI not just automated way) - Redact text (Via UI not just automated way)
- Add Forms - Add Forms
- Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing - Multi page layout (Stich PDF pages together) support x rows y columns and custom page sizing
- Fill forms manually or automatically - Fill forms mannual and automatic
### Q2: Why is my application downloading .htm files? ### Q2: Why is my application downloading .htm files?
This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files. This is an issue caused commonly by your NGINX configuration. The default file upload size for NGINX is 1MB, you need to add the following in your Nginx sites-available file. ``client_max_body_size SIZE;`` Where "SIZE" is 50M for example for 50MB files.
### Q3: Why is my download timing out ### Q3: Why is my download timing out
NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;`` NGINX has timeout values by default so if you are running Stirling-PDF behind NGINX you may need to set a timeout value such as adding the config ``proxy_read_timeout 3600;``

View File

@@ -1,52 +1,64 @@
| Technology | Ultra-Lite | Full | |Technology | Ultra-Lite | Lite | Full |
|----------------|:----------:|:----:| |----------------|:----------:|:----:|:----:|
| Java | ✔️ | ✔️ | | Java | ✔️ | ✔️ | ✔️ |
| JavaScript | ✔️ | ✔️ | | JavaScript | ✔️ | ✔️ | ✔️ |
| Libre | | ✔️ | | Libre | | ✔️ | ✔️ |
| Python | | ✔️ | | Python | | | ✔️ |
| OpenCV | | ✔️ | | OpenCV | | | ✔️ |
| OCRmyPDF | | ✔️ | | OCRmyPDF | | | ✔️ |
Operation | Ultra-Lite | Full
-------------------------|------------|-----
add-page-numbers | ✔️ | ✔️
add-password | ✔️ | ✔️
add-image | ✔️ | ✔️ Operation | Ultra-Lite | Lite | Full
add-watermark | ✔️ | ✔️ --------------------|------------|------|-----
adjust-contrast | ✔️ | ✔️ add-page-numbers | ✔️ | ✔️ | ✔️
auto-split-pdf | ✔️ | ✔️ add-password | ✔️ | ✔️ | ✔️
auto-redact | ✔️ | ✔️ add-image | ✔️ | ✔️ | ✔️
auto-rename | ✔️ | ✔️ add-watermark | ✔️ | ✔️ | ✔️
cert-sign | ✔️ | ✔️ adjust-contrast | ✔️ | ✔️ | ✔️
crop | ✔️ | ✔️ auto-split-pdf | ✔️ | ✔️ | ✔️
change-metadata | ✔️ | ✔️ auto-redact | ✔️ | ✔️ | ✔️
change-permissions | ✔️ | ✔️ auto-rename | ✔️ | ✔️ | ✔️
compare | ✔️ | ✔️ cert-sign | ✔️ | ✔️ | ✔️
extract-page | ✔️ | ✔️ crop | ✔️ | ✔️ | ✔️
extract-images | ✔️ | ✔️ change-metadata | ✔️ | ✔️ | ✔️
flatten | ✔️ | ✔️ change-permissions | ✔️ | ✔️ | ✔️
get-info-on-pdf | ✔️ | ✔️ compare | ✔️ | ✔️ | ✔️
img-to-pdf | ✔️ | ✔️ extract-page | ✔️ | ✔️ | ✔️
markdown-to-pdf | ✔️ | ✔️ extract-images | ✔️ | ✔️ | ✔️
merge-pdfs | ✔️ | ✔️ flatten | ✔️ | ✔️ | ✔️
multi-page-layout | ✔️ | ✔️ get-info-on-pdf | ✔️ | ✔️ | ✔️
overlay-pdf | ✔️ | ✔️ img-to-pdf | ✔️ | ✔️ | ✔️
pdf-organizer | ✔️ | ✔️ markdown-to-pdf | ✔️ | ✔️ | ✔️
pdf-to-csv | ✔️ | ✔️ merge-pdfs | ✔️ | ✔️ | ✔️
pdf-to-img | ✔️ | ✔️ multi-page-layout | ✔️ | ✔️ | ✔️
pdf-to-single-page | ✔️ | ✔️ overlay-pdf | ✔️ | ✔️ | ✔️
remove-pages | ✔️ | ✔️ pdf-organizer | ✔️ | ✔️ | ✔️
remove-password | ✔️ | ✔️ pdf-to-csv | ✔️ | ✔️ | ✔️
rotate-pdf | ✔️ | ✔️ pdf-to-img | ✔️ | ✔️ | ✔️
sanitize-pdf | ✔️ | ✔️ pdf-to-single-page | ✔️ | ✔️ | ✔️
scale-pages | ✔️ | ✔️ remove-pages | ✔️ | ✔️ | ✔️
sign | ✔️ | ✔️ remove-password | ✔️ | ✔️ | ✔️
show-javascript | ✔️ | ✔️ rotate-pdf | ✔️ | ✔️ | ✔️
split-by-size-or-count | ✔️ | ✔️ sanitize-pdf | ✔️ | ✔️ | ✔️
split-pdf-by-sections | ✔️ | ✔️ scale-pages | ✔️ | ✔️ | ✔️
split-pdfs | ✔️ | ✔️ sign | ✔️ | ✔️ | ✔️
compress-pdf | | ✔️ show-javascript | ✔️ | ✔️ | ✔️
extract-image-scans | | ✔️ split-by-size-or-count | ✔️ | ✔️ | ✔️
ocr-pdf | | ✔️ split-pdf-by-sections | ✔️ | ✔️ | ✔️
pdf-to-pdfa | | ✔️ split-pdfs | ✔️ | ✔️ | ✔️
remove-blanks | | ✔️ file-to-pdf | | ✔️ | ✔️
pdf-to-html | | ✔️ | ✔️
pdf-to-presentation | | ✔️ | ✔️
pdf-to-text | | ✔️ | ✔️
pdf-to-word | | ✔️ | ✔️
pdf-to-xml | | ✔️ | ✔️
repair | | ✔️ | ✔️
xlsx-to-pdf | | ✔️ | ✔️
compress-pdf | | | ✔️
extract-image-scans | | | ✔️
ocr-pdf | | | ✔️
pdf-to-pdfa | | | ✔️
remove-blanks | | | ✔️

View File

@@ -1,31 +1,21 @@
plugins { plugins {
id 'java' id 'java'
id 'org.springframework.boot' version '3.2.4' id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.3' id 'io.spring.dependency-management' version '1.1.3'
id 'org.springdoc.openapi-gradle-plugin' version '1.8.0' id 'org.springdoc.openapi-gradle-plugin' version '1.8.0'
id "io.swagger.swaggerhub" version "1.3.2" id "io.swagger.swaggerhub" version "1.3.2"
id 'edu.sc.seis.launch4j' version '3.0.5' id 'edu.sc.seis.launch4j' version '3.0.5'
id 'com.diffplug.spotless' version '6.25.0' id 'com.diffplug.spotless' version '6.23.3'
id 'com.github.jk1.dependency-license-report' version '2.6'
} }
import com.github.jk1.license.render.*
group = 'stirling.software' group = 'stirling.software'
version = '0.24.6' version = '0.18.1'
//17 is lowest but we support and recommend 21
sourceCompatibility = '17' sourceCompatibility = '17'
repositories { repositories {
mavenCentral() mavenCentral()
} }
licenseReport {
renderers = [new JsonReportRenderer()]
}
sourceSets { sourceSets {
main { main {
java { java {
@@ -49,6 +39,7 @@ openApi {
outputFileName = "SwaggerDoc.json" outputFileName = "SwaggerDoc.json"
} }
launch4j { launch4j {
icon = "${projectDir}/src/main/resources/static/favicon.ico" icon = "${projectDir}/src/main/resources/static/favicon.ico"
@@ -56,8 +47,8 @@ launch4j {
headerType="console" headerType="console"
jarTask = tasks.bootJar jarTask = tasks.bootJar
errTitle="Encountered error, Do you have Java 21?" errTitle="Encountered error, Do you have Java 17?"
downloadUrl="https://download.oracle.com/java/21/latest/jdk-21_windows-x64_bin.exe" downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"
variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"] variables=["BROWSER_OPEN=true", "ENDPOINTS_GROUPS_TO_REMOVE=CLI"]
jreMinVersion="17" jreMinVersion="17"
@@ -66,8 +57,8 @@ launch4j {
messagesStartupError="An error occurred while starting Stirling-PDF" messagesStartupError="An error occurred while starting Stirling-PDF"
//messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17." //messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17."
messagesJreVersionError="You are running the wrong version of Java, Please download Java 21." messagesJreVersionError="You are running the wrong version of Java, Please download Java 17."
messagesLauncherError="Java is corrupted. Please uninstall and then install Java 21." messagesLauncherError="Java is corrupted. Please uninstall and then install Java 17."
messagesInstanceAlreadyExists="Stirling-PDF is already running." messagesInstanceAlreadyExists="Stirling-PDF is already running."
} }
@@ -87,27 +78,22 @@ spotless {
dependencies { dependencies {
//security updates //security updates
implementation 'ch.qos.logback:logback-classic:1.5.3' implementation 'ch.qos.logback:logback-classic:1.4.14'
implementation 'ch.qos.logback:logback-core:1.5.3' implementation 'ch.qos.logback:logback-core:1.4.14'
implementation 'org.springframework:spring-webmvc:6.1.5' implementation 'org.springframework:spring-webmvc:6.0.15'
implementation("io.github.pixee:java-security-toolkit:1.1.3") implementation 'org.yaml:snakeyaml:2.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
implementation 'org.yaml:snakeyaml:2.2' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.1'
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.4'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.2.4'
if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') {
implementation 'org.springframework.boot:spring-boot-starter-security:3.2.4' implementation 'org.springframework.boot:spring-boot-starter-security:3.2.1'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter-data-jpa:3.2.4" implementation "org.springframework.boot:spring-boot-starter-data-jpa"
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client:3.2.4' implementation "com.h2database:h2"
//2.2.x requires rebuild of DB file.. need migration path
implementation "com.h2database:h2:2.1.214"
} }
testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.4' testImplementation 'org.springframework.boot:spring-boot-starter-test:3.2.1'
// Batik // Batik
implementation 'org.apache.xmlgraphics:batik-all:1.17' implementation 'org.apache.xmlgraphics:batik-all:1.17'
@@ -136,42 +122,36 @@ dependencies {
//general PDF //general PDF
// https://mvnrepository.com/artifact/com.opencsv/opencsv // https://mvnrepository.com/artifact/com.opencsv/opencsv
implementation ('com.opencsv:opencsv:5.9') { implementation ('com.opencsv:opencsv:5.7.1') {
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation ('org.apache.pdfbox:pdfbox:3.0.2'){ implementation ('org.apache.pdfbox:pdfbox:2.0.29'){
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation ('org.apache.pdfbox:xmpbox:3.0.2'){ implementation ('org.apache.pdfbox:xmpbox:2.0.29'){
exclude group: 'commons-logging', module: 'commons-logging' exclude group: 'commons-logging', module: 'commons-logging'
} }
implementation 'org.bouncycastle:bcprov-jdk18on:1.77' implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77' implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
implementation 'org.springframework.boot:spring-boot-starter-actuator:3.2.4' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-core:1.12.4' implementation 'io.micrometer:micrometer-core'
implementation group: 'com.google.zxing', name: 'core', version: '3.5.3' implementation group: 'com.google.zxing', name: 'core', version: '3.5.2'
// https://mvnrepository.com/artifact/org.commonmark/commonmark // https://mvnrepository.com/artifact/org.commonmark/commonmark
implementation 'org.commonmark:commonmark:0.22.0' implementation 'org.commonmark:commonmark:0.21.0'
implementation 'org.commonmark:commonmark-ext-gfm-tables:0.22.0'
// https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core // https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core
implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0' implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0'
implementation 'com.fathzer:javaluator:3.0.3' developmentOnly("org.springframework.boot:spring-boot-devtools")
compileOnly 'org.projectlombok:lombok:1.18.30'
developmentOnly("org.springframework.boot:spring-boot-devtools:3.2.4") annotationProcessor 'org.projectlombok:lombok:1.18.28'
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
} }
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
dependsOn 'spotlessApply' dependsOn 'spotlessApply'
} }
compileJava {
options.compilerArgs << '-parameters'
}
task writeVersion { task writeVersion {
def propsFile = file('src/main/resources/version.properties') def propsFile = file('src/main/resources/version.properties')

View File

@@ -1,16 +1,15 @@
apiVersion: v2 apiVersion: v2
appVersion: 0.24.5 appVersion: 0.14.2
description: locally hosted web application that allows you to perform various operations description: locally hosted web application that allows you to perform various operations on PDF files
on PDF files home: https://github.com/Frooodle/Stirling-PDF
home: https://github.com/Stirling-Tools/Stirling-PDF
keywords: keywords:
- stirling-pdf - stirling-pdf
- helm - helm
- charts repo - charts repo
maintainers: maintainers:
- name: Stirling-Tools - name: Frooodle
url: https://github.com/Stirling-Tools/Stirling-PDF url: https://github.com/Frooodle/Stirling-PDF
name: stirling-pdf-chart name: stirling-pdf-chart
sources: sources:
- https://github.com/Stirling-Tools/Stirling-PDF - https://github.com/Frooodle/Stirling-PDF
version: 1.0.0 version: 1.0.0

View File

@@ -43,6 +43,6 @@ spec:
name: http name: http
{{- end }} {{- end }}
protocol: TCP protocol: TCP
selector: selector:
{{- include "stirlingpdf.selectorLabels" . | nindent 4 }} {{- include "stirlingpdf.selectorLabels" . | nindent 4 }}

View File

@@ -16,11 +16,11 @@ commonLabels: {}
# team_name: dev # team_name: dev
envs: [] envs: []
# - name: UI_APP_NAME # - name: PP_HOME_NAME
# value: "Stirling PDF" # value: "Stirling PDF"
# - name: UI_HOME_DESCRIPTION # - name: APP_HOME_DESCRIPTION
# value: "Your locally hosted one-stop-shop for all your PDF needs." # value: "Your locally hosted one-stop-shop for all your PDF needs."
# - name: UI_APP_NAVBAR_NAME # - name: APP_NAVBAR_NAME
# value: "Stirling PDF" # value: "Stirling PDF"
# - name: ALLOW_GOOGLE_VISIBILITY # - name: ALLOW_GOOGLE_VISIBILITY
# value: "true" # value: "true"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,110 +1,310 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg <svg
width="99.537987mm"
height="95.209366mm"
viewBox="0 0 99.537987 95.209366"
version="1.1" version="1.1"
id="Layer_1" id="svg745"
x="0px"
y="0px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
xml:space="preserve" xml:space="preserve"
sodipodi:docname="favicon.svg" inkscape:version="1.2.1 (9c6d41e4, 2022-07-14)"
inkscape:version="1.2.2 (732a01da63, 2022-12-09)" sodipodi:docname="stirling.svg"
inkscape:export-filename="favicon.png" inkscape:export-filename="stirling.png"
inkscape:export-xdpi="96" inkscape:export-xdpi="80"
inkscape:export-ydpi="96" inkscape:export-ydpi="80"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="defs173"> id="namedview747"
pagecolor="#ffffff"
bordercolor="#666666"
<linearGradient borderopacity="1.0"
id="XMLID_5_" inkscape:showpageshadow="2"
gradientUnits="userSpaceOnUse" inkscape:pageopacity="0.0"
x1="304.496" inkscape:pagecheckerboard="0"
y1="422.9102" inkscape:deskcolor="#d1d1d1"
x2="316.036" inkscape:document-units="mm"
y2="326.2626"> showgrid="false"
<stop inkscape:zoom="0.914906"
offset="0" inkscape:cx="184.17193"
style="stop-color:#DCF1F3" inkscape:cy="509.88845"
id="stop156" /> inkscape:window-width="2293"
<stop inkscape:window-height="1387"
offset="1" inkscape:window-x="122"
style="stop-color:#C2C2C9" inkscape:window-y="25"
id="stop158" /> inkscape:window-maximized="0"
</linearGradient> inkscape:current-layer="svg745" /><defs
id="defs742"><linearGradient
</defs><sodipodi:namedview inkscape:collect="always"
id="namedview171" id="linearGradient72198"><stop
pagecolor="#ffffff" style="stop-color:#ccd6d7;stop-opacity:1;"
bordercolor="#000000" offset="0"
borderopacity="0.25" id="stop72194" /><stop
inkscape:showpageshadow="2" style="stop-color:#0f3a3f;stop-opacity:1;"
inkscape:pageopacity="0.0" offset="1"
inkscape:pagecheckerboard="0" id="stop72196" /></linearGradient><linearGradient
inkscape:deskcolor="#d1d1d1" inkscape:collect="always"
showgrid="false" id="linearGradient71928"><stop
inkscape:zoom="1.4142136" style="stop-color:#4b0005;stop-opacity:1;"
inkscape:cx="219.91021" offset="0"
inkscape:cy="232.63813" id="stop71924" /><stop
inkscape:window-width="3840" style="stop-color:#8f000c;stop-opacity:1;"
inkscape:window-height="2054" offset="1"
inkscape:window-x="2869" id="stop71926" /></linearGradient><linearGradient
inkscape:window-y="-11" inkscape:collect="always"
inkscape:window-maximized="1" id="linearGradient71920"><stop
inkscape:current-layer="XMLID_4_" /> style="stop-color:#4b0005;stop-opacity:1;"
<style offset="0"
type="text/css" id="stop71916" /><stop
id="style150"> style="stop-color:#8f000c;stop-opacity:1;"
.st0{fill:#FFFFFF;} offset="1"
.st1{fill:#C02223;} id="stop71918" /></linearGradient><linearGradient
.st2{fill:#882425;} inkscape:collect="always"
.st3{fill:url(#XMLID_5_);} id="linearGradient69598"><stop
.st4{fill:url(#XMLID_7_);} style="stop-color:#6a0007;stop-opacity:1;"
</style> offset="0"
id="stop69594" /><stop
<g style="stop-color:#b8000f;stop-opacity:1;"
id="XMLID_4_"> offset="1"
<path id="stop69596" /></linearGradient><linearGradient
id="XMLID_131_" inkscape:collect="always"
class="st1" id="linearGradient46361"><stop
d="M 347.01402,14.355825 98.978019,69.02261 C 73.825483,74.547445 55.942464,96.792175 55.942464,122.52628 v 315.06096 c 0,22.39012 16.719895,41.14548 38.819234,43.76251 L 224.8861,498.36042 339.48636,384.26465 455.76603,265.15425 453.73057,84.870162 C 453.43979,62.916214 433.08513,46.632491 411.71274,51.284984 l -28.78729,6.251786 0.14539,-13.666697 C 383.36162,24.678542 365.62399,10.284894 347.01402,14.355825 Z" style="stop-color:#f7f6f8;stop-opacity:1;"
sodipodi:nodetypes="ccssccccccccc" offset="0"
style="stroke-width:1.45391" /><path id="stop46359" /><stop
id="XMLID_117_" style="stop-color:#b7b7b5;stop-opacity:1;"
class="st2" offset="1"
d="m 383.21622,57.53677 v 285.8375 L 456.05681,265.00885 454.02135,78.763767 C 453.87595,59.863016 436.28372,45.905539 417.81914,49.97647 Z" id="stop46357" /></linearGradient><linearGradient
style="stroke-width:1.45391" /><polygon inkscape:collect="always"
id="XMLID_18_" id="linearGradient40554"><stop
class="st3" style="stop-color:#f4f2f4;stop-opacity:1;"
points="234.7,422.6 368.5,387.7 393.5,262.2 " offset="0"
style="fill:url(#XMLID_5_)" id="stop40550" /><stop
transform="matrix(1.4556308,0,0,1.4548265,-116.73161,-116.45231)" /> style="stop-color:#57767b;stop-opacity:1;"
<linearGradient offset="1"
id="XMLID_7_" id="stop40552" /></linearGradient><linearGradient
gradientUnits="userSpaceOnUse" inkscape:collect="always"
x1="223.0838" id="linearGradient39095"><stop
y1="372.7559" style="stop-color:#285459;stop-opacity:1;"
x2="241.4174" offset="0"
y2="114.557" id="stop39093" /><stop
gradientTransform="matrix(1.4539039,0,0,1.4539039,-116.19976,-116.20474)"> style="stop-color:#a6b6af;stop-opacity:1;"
<stop offset="1"
offset="0" id="stop39091" /></linearGradient><linearGradient
style="stop-color:#DCF1F3" inkscape:collect="always"
id="stop163" /> id="linearGradient36672"><stop
<stop style="stop-color:#da453f;stop-opacity:1;"
offset="1" offset="0"
style="stop-color:#C2C2C9" id="stop36668" /><stop
id="stop165" /> style="stop-color:#a60008;stop-opacity:1;"
</linearGradient> offset="1"
<path id="stop36670" /></linearGradient><linearGradient
id="XMLID_6_" inkscape:collect="always"
class="st4" id="linearGradient19524"><stop
d="m 282.89686,214.84917 c 0,0 -22.24473,-28.93269 -38.67384,-36.78377 -10.46811,-4.94327 -26.02489,-6.83335 -38.23768,-0.72695 -18.02841,9.0142 -19.91848,34.31213 -3.34397,44.34406 3.92553,2.47165 9.15959,4.50711 15.99294,6.10641 36.63838,8.43264 97.12077,25.87949 89.70587,96.10304 0,0 -4.21633,65.86185 -73.56753,73.42215 -12.2128,1.30851 -24.57098,0.43617 -36.493,-2.32625 -16.42911,-3.63476 -45.50719,-11.04967 -59.75545,-19.91849 l -2.61703,-75.16682 h 6.97875 c 0,0 13.81208,33.43978 53.06749,49.57812 7.26952,2.90781 15.26599,4.07093 22.97168,2.90781 9.74116,-1.45391 21.22699,-6.68796 25.87949,-22.53551 0,0 7.85108,-23.11707 -32.85823,-35.76604 -32.56744,-10.17733 -63.24481,-20.64543 -75.89378,-54.95757 -5.961,-16.28371 -6.97874,-34.31212 -2.90781,-51.61358 5.37944,-22.53551 20.79082,-54.23062 64.40794,-67.89732 0,0 57.28381,-15.55677 96.53922,5.52484 l -1.74468,89.70587 z" style="stop-color:#3a4b4f;stop-opacity:1;"
style="fill:url(#XMLID_7_);stroke-width:1.45391" /> offset="0"
</g> id="stop19522" /><stop
</svg> style="stop-color:#617979;stop-opacity:0.97461927;"
offset="1"
id="stop19520" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient17996"><stop
style="stop-color:#401016;stop-opacity:1;"
offset="0"
id="stop17994" /><stop
style="stop-color:#761f19;stop-opacity:1;"
offset="1"
id="stop17992" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient15569"><stop
style="stop-color:#8ea8ad;stop-opacity:1;"
offset="0"
id="stop15565" /><stop
style="stop-color:#e9e7eb;stop-opacity:1;"
offset="1"
id="stop15567" /></linearGradient><linearGradient
inkscape:collect="always"
id="linearGradient15557"><stop
style="stop-color:#9b0e11;stop-opacity:1;"
offset="0"
id="stop15553" /><stop
style="stop-color:#370707;stop-opacity:1;"
offset="1"
id="stop15555" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient15557"
id="linearGradient15559"
x1="120.06672"
y1="63.25761"
x2="135.16347"
y2="78.078682"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient15569"
id="linearGradient15571"
x1="127.97037"
y1="101.66144"
x2="133.88971"
y2="104.77026"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient17996"
id="linearGradient17998"
x1="117.9284"
y1="86.055084"
x2="130.67392"
y2="76.945541"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient19524"
id="linearGradient19528"
x1="130.98172"
y1="82.402977"
x2="135.72115"
y2="86.45166"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient36672"
id="linearGradient36674"
x1="63.797714"
y1="74.81752"
x2="96.636673"
y2="120.29293"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient39095"
id="linearGradient39097"
x1="120.54506"
y1="124.76902"
x2="128.04152"
y2="126.0704"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient40554"
id="linearGradient40556"
x1="113.84585"
y1="114.04285"
x2="119.65858"
y2="128.50244"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient46361"
id="linearGradient46363"
x1="73.993439"
y1="114.13906"
x2="94.845322"
y2="71.247383"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient69598"
id="linearGradient69600"
x1="95.854446"
y1="114.66749"
x2="103.77842"
y2="120.1887"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient71920"
id="linearGradient71922"
x1="98.580376"
y1="87.186958"
x2="118.09738"
y2="101.19449"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient71928"
id="linearGradient71930"
x1="78.278267"
y1="97.433273"
x2="92.313202"
y2="104.33479"
gradientUnits="userSpaceOnUse" /><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient72198"
id="linearGradient72200"
x1="125.76636"
y1="138.46817"
x2="123.3327"
y2="126.03291"
gradientUnits="userSpaceOnUse" /></defs><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="background"
style="display:inline"
sodipodi:insensitive="true"
transform="translate(-51.420144,-44.470286)"><rect
style="display:inline;fill:#ccd6d7;fill-opacity:1;stroke:none;stroke-width:4.13755;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:3.2"
id="rect72067"
width="99.481979"
height="94.999512"
x="51.476147"
y="44.680138" /></g><g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="shadow"
style="display:inline"
sodipodi:insensitive="true"
transform="translate(-51.420144,-44.470286)"><path
style="display:inline;fill:url(#linearGradient72200);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 84.146049,134.73858 c 0,0 11.721038,2.48294 17.938661,2.91673 6.21763,0.43378 14.75251,0.59994 22.41237,-0.43379 8.01008,-1.081 13.19907,-2.22733 14.50043,-2.66112 1.30136,-0.43379 4.00784,-1.24297 4.15244,-2.25514 0.1446,-1.01217 -3.4703,-2.74733 -6.21763,-3.32571 -2.74732,-0.57838 -12.72444,-1.44596 -14.89337,-1.44596 -2.16894,0 -37.892901,7.20499 -37.892901,7.20499 z"
id="path72192"
sodipodi:nodetypes="cssssssc" /></g><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Origami"
style="display:inline"
sodipodi:insensitive="true"
transform="translate(-51.420144,-44.470286)"><path
style="display:inline;fill:url(#linearGradient40556);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 84.460552,134.86721 35.165798,-6.85679 16.15467,-42.7383 z"
id="path960"
sodipodi:nodetypes="cccc" /><path
style="fill:url(#linearGradient15571);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 135.71493,85.428056 0.3984,45.049024 -9.66213,-20.46173 z"
id="path964"
sodipodi:nodetypes="cccc" /><path
style="display:inline;fill:url(#linearGradient39097);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 119.60518,128.00293 16.5337,2.48693 -9.68769,-20.5512 z"
id="path966"
sodipodi:nodetypes="cccc" /><path
style="display:inline;fill:url(#linearGradient15559);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 118.42209,57.022622 12.70423,-2.766809 -0.0785,25.087175 -12.43878,-13.376518 z"
id="path968"
sodipodi:nodetypes="ccccc" /><path
style="display:inline;fill:url(#linearGradient19528);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 135.72114,85.386768 -4.84219,-6.459493 0.0305,11.126604 z"
id="path970"
sodipodi:nodetypes="cccc" /><path
style="display:inline;fill:url(#linearGradient17998);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 119.10273,65.682415 11.96883,13.44935 -0.0899,10.819868 -11.88577,11.430427 z"
id="path972"
sodipodi:nodetypes="ccccc" /><path
style="display:inline;fill:url(#linearGradient36674);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 62.145635,130.15618 62.043392,65.435258 c 0,0 0.179321,-2.778132 1.89516,-4.036097 1.874923,-1.374597 4.341768,-1.894096 4.341768,-1.894096 l 50.91788,-11.349167 -0.0113,53.144272 -34.733274,33.56547 z"
id="path958"
sodipodi:nodetypes="ccsccccc" /></g><g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Letter"
style="display:inline"
sodipodi:insensitive="true"
transform="translate(-51.420144,-44.470286)"><path
style="display:inline;fill:url(#linearGradient69600);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 94.780881,91.406803 16.870379,17.074877 -23.723345,23.00249 -18.122131,-17.99816 5.497473,-2.80607 18.404054,-0.0511 2.35163,-8.23071 z"
id="path54894"
sodipodi:nodetypes="cccccccc" /><path
style="display:inline;fill:url(#linearGradient71930);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 72.440405,92.224764 16.15467,15.745686 4.089788,-6.79927 z"
id="path54892" /><path
style="display:inline;fill:url(#linearGradient71922);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.138739,84.965385 1.124691,-14.109776 22.92453,22.286787 0.008,8.164604 -3.28863,3.16649 z"
id="path54890"
sodipodi:nodetypes="cccccc"
inkscape:label="path54890" /><path
style="display:inline;fill:url(#linearGradient46363);fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 95.138739,84.965385 h 1.226936 l -0.05112,-14.109776 c 0,0 -5.776827,-3.220709 -12.167126,-2.40275 -6.390296,0.817957 -8.151582,2.1248 -10.58233,4.396523 -1.90229,1.777838 -2.913974,3.527446 -3.987546,7.157132 -0.512646,1.733226 -0.281963,5.988892 0.613471,8.537436 0.664591,1.891528 2.453873,4.294281 4.958868,6.134686 2.662335,1.956002 8.281825,3.527443 8.281825,3.527443 0,0 5.134614,1.887351 5.572338,4.294281 0.308137,1.69437 -0.102243,3.22071 -1.635914,4.95887 -1.258314,1.42609 -3.62969,1.99377 -6.288054,1.07357 -2.658364,-0.92021 -6.139514,-3.85065 -7.106009,-4.90775 -0.817958,-0.89464 -2.820665,-3.06173 -2.890231,-4.294021 -0.01209,-0.214205 -1.229505,-0.0963 -1.229505,-0.0963 l -0.0723,14.256941 5.879073,2.24938 c 0,0 5.214483,1.78929 8.946415,1.43143 3.731934,-0.35786 7.617235,-0.51122 11.604778,-5.16336 3.987542,-4.65213 3.680812,-12.831715 2.147141,-15.899056 -1.533673,-3.067344 -3.561212,-6.138812 -10.480087,-8.281826 -5.776829,-1.789283 -7.872846,-3.01622 -8.128458,-4.396524 -0.255611,-1.380305 0.0091,-4.253646 2.760607,-5.214481 3.220711,-1.124693 5.623462,-0.05112 7.05489,1.12469 1.431425,1.175817 5.572339,5.623462 5.572339,5.623462 z"
id="path46355"
sodipodi:nodetypes="cccssssscssssscccssssssscc" /></g></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,40 +0,0 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SECURITY_OAUTH2_ENABLED: "true"
SECURITY_OAUTH2_AUTOCREATEUSER: "true" # This is set to true to allow auto-creation of non-existing users in Stirling-PDF
SECURITY_OAUTH2_ISSUER: "https://accounts.google.com" # Change with any other provider that supports OpenID Connect Discovery (/.well-known/openid-configuration) end-point
SECURITY_OAUTH2_CLIENTID: "<YOUR CLIENT ID>.apps.googleusercontent.com" # Client ID from your provider
SECURITY_OAUTH2_CLIENTSECRET: "<YOUR CLIENT SECRET>" # Client Secret from your provider
SECURITY_OAUTH2_SCOPES: "openid,profile,email" # Expected OAuth2 Scope
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -1,34 +0,0 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Security
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
PUID: 1002
PGID: 1002
UMASK: "022"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -1,31 +0,0 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite-Security
image: frooodle/s-pdf:latest-ultra-lite
deploy:
resources:
limits:
memory: 1G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -q 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "true"
SECURITY_ENABLELOGIN: "true"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF-Lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Lite Latest with Security
UI_APPNAMENAVBAR: Stirling-PDF-Lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -1,30 +0,0 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF-Ultra-Lite
image: frooodle/s-pdf:latest-ultra-lite
deploy:
resources:
limits:
memory: 1G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF-Ultra-lite
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF-Ultra-lite Latest
UI_APPNAMENAVBAR: Stirling-PDF-Ultra-lite Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

View File

@@ -1,33 +0,0 @@
version: '3.3'
services:
stirling-pdf:
container_name: Stirling-PDF
image: frooodle/s-pdf:latest
deploy:
resources:
limits:
memory: 4G
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/api/v1/info/status | grep -q 'UP' && curl -fL http://localhost:8080/ | grep -qv 'Please sign in'"]
interval: 5s
timeout: 10s
retries: 16
ports:
- 8080:8080
volumes:
- /stirling/latest/data:/usr/share/tessdata:rw
- /stirling/latest/config:/configs:rw
- /stirling/latest/logs:/logs:rw
environment:
DOCKER_ENABLE_SECURITY: "false"
SECURITY_ENABLELOGIN: "false"
LANGS: "en_GB,en_US,ar_AR,de_DE,fr_FR,es_ES,zh_CN,zh_TW,ca_CA,it_IT,sv_SE,pl_PL,ro_RO,ko_KR,pt_BR,ru_RU,el_GR,hi_IN,hu_HU,tr_TR,id_ID"
INSTALL_BOOK_AND_ADVANCED_HTML_OPS: "true"
SYSTEM_DEFAULTLOCALE: en-US
UI_APPNAME: Stirling-PDF
UI_HOMEDESCRIPTION: Demo site for Stirling-PDF Latest
UI_APPNAMENAVBAR: Stirling-PDF Latest
SYSTEM_MAXFILESIZE: "100"
METRICS_ENABLED: "true"
SYSTEM_GOOGLEVISIBILITY: "true"
restart: on-failure:5

0
gradlew vendored Executable file → Normal file
View File

182
gradlew.bat vendored
View File

@@ -1,91 +1,91 @@
@rem @rem
@rem Copyright 2015 the original author or authors. @rem Copyright 2015 the original author or authors.
@rem @rem
@rem Licensed under the Apache License, Version 2.0 (the "License"); @rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem https://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter. @rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if %ERRORLEVEL% equ 0 goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1 if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE% exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

BIN
images/stirling-home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@@ -6,8 +6,7 @@
"parameters": { "parameters": {
"horizontalDivisions": 2, "horizontalDivisions": 2,
"verticalDivisions": 2, "verticalDivisions": 2,
"fileInput": "automated", "fileInput": "automated"
"merge": false
} }
}, },
{ {
@@ -31,4 +30,4 @@
}, },
"outputDir": "{outputFolder}", "outputDir": "{outputFolder}",
"outputFileName": "{filename}" "outputFileName": "{filename}"
} }

View File

@@ -16,7 +16,7 @@ public class PropSync {
Map<String, String> enProps = linesToProps(enLines); Map<String, String> enProps = linesToProps(enLines);
for (File file : files) { for (File file : files) {
if (!"messages_en_GB.properties".equals(file.getName())) { if (!file.getName().equals("messages_en_GB.properties")) {
System.out.println("Processing file: " + file.getName()); System.out.println("Processing file: " + file.getName());
List<String> lines; List<String> lines;
try { try {

View File

@@ -1,192 +0,0 @@
"""A script to update language progress status in README.md based on
properties file comparison.
This script compares default properties file with others in a directory to
determine language progress.
It then updates README.md based on provided progress list.
Author: Ludy87
Example:
To use this script, simply run it from command line:
$ python counter_translation.py
""" # noqa: D205
import glob
import os
import re
import tomlkit
import tomlkit.toml_file
def convert_to_multiline(data: tomlkit.TOMLDocument) -> tomlkit.TOMLDocument:
"""Converts 'ignore' and 'missing' arrays to multiline arrays and sorts the first-level keys of the TOML document.
Enhances readability and consistency in the TOML file by ensuring arrays contain unique and sorted entries.
Parameters:
data (tomlkit.TOMLDocument): The original TOML document containing the data.
Returns:
tomlkit.TOMLDocument: A new TOML document with sorted keys and properly formatted arrays.
""" # noqa: D205
sorted_data = tomlkit.document()
for key in sorted(data.keys()):
value = data[key]
if isinstance(value, dict):
new_table = tomlkit.table()
for subkey in ("ignore", "missing"):
if subkey in value:
# Convert the list to a set to remove duplicates, sort it, and convert to multiline for readability
unique_sorted_array = sorted(set(value[subkey]))
array = tomlkit.array()
array.multiline(True)
for item in unique_sorted_array:
array.append(item)
new_table[subkey] = array
sorted_data[key] = new_table
else:
# Add other types of data unchanged
sorted_data[key] = value
return sorted_data
def write_readme(progress_list: list[tuple[str, int]]) -> None:
"""Updates the progress status in the README.md file based
on the provided progress list.
Parameters:
progress_list (list[tuple[str, int]]): A list of tuples containing
language and progress percentage.
Returns:
None
""" # noqa: D205
with open("README.md", encoding="utf-8") as file:
content = file.readlines()
for i, line in enumerate(content[2:], start=2):
for progress in progress_list:
language, value = progress
if language in line:
if match := re.search(r"\!\[(\d+(\.\d+)?)%\]\(.*\)", line):
content[i] = line.replace(
match.group(0),
f"![{value}%](https://geps.dev/progress/{value})",
)
with open("README.md", "w", encoding="utf-8") as file:
file.writelines(content)
def compare_files(default_file_path, file_paths, translation_status_file) -> list[tuple[str, int]]:
"""Compares the default properties file with other
properties files in the directory.
Parameters:
default_file_path (str): The path to the default properties file.
files_directory (str): The directory containing other properties files.
Returns:
list[tuple[str, int]]: A list of tuples containing
language and progress percentage.
""" # noqa: D205
num_lines = sum(
1 for line in open(default_file_path, encoding="utf-8") if line.strip() and not line.strip().startswith("#")
)
result_list = []
sort_translation_status: tomlkit.TOMLDocument
# read toml
with open(translation_status_file, encoding="utf-8") as f:
sort_translation_status = tomlkit.parse(f.read())
for file_path in file_paths:
language = os.path.basename(file_path).split("messages_", 1)[1].split(".properties", 1)[0]
fails = 0
if "en_GB" in language or "en_US" in language:
result_list.append(("en_GB", 100))
result_list.append(("en_US", 100))
continue
if language not in sort_translation_status:
sort_translation_status[language] = tomlkit.table()
if (
"ignore" not in sort_translation_status[language]
or len(sort_translation_status[language].get("ignore", [])) < 1
):
sort_translation_status[language]["ignore"] = tomlkit.array(["language.direction"])
# if "missing" not in sort_translation_status[language]:
# sort_translation_status[language]["missing"] = tomlkit.array()
# elif "language.direction" in sort_translation_status[language]["missing"]:
# sort_translation_status[language]["missing"].remove("language.direction")
with open(default_file_path, encoding="utf-8") as default_file, open(file_path, encoding="utf-8") as file:
for _ in range(5):
next(default_file)
try:
next(file)
except StopIteration:
fails = num_lines
for line_num, (line_default, line_file) in enumerate(zip(default_file, file), start=6):
try:
# Ignoring empty lines and lines start with #
if line_default.strip() == "" or line_default.startswith("#"):
continue
default_key, default_value = line_default.split("=", 1)
file_key, file_value = line_file.split("=", 1)
if (
default_value.strip() == file_value.strip()
and default_key.strip() not in sort_translation_status[language]["ignore"]
):
print(f"{language}: Line {line_num} is missing the translation.")
# if default_key.strip() not in sort_translation_status[language]["missing"]:
# missing_array = tomlkit.array()
# missing_array.append(default_key.strip())
# missing_array.multiline(True)
# sort_translation_status[language]["missing"].extend(missing_array)
fails += 1
# elif default_key.strip() in sort_translation_status[language]["ignore"]:
# if default_key.strip() in sort_translation_status[language]["missing"]:
# sort_translation_status[language]["missing"].remove(default_key.strip())
if default_value.strip() != file_value.strip():
# if default_key.strip() in sort_translation_status[language]["missing"]:
# sort_translation_status[language]["missing"].remove(default_key.strip())
if default_key.strip() in sort_translation_status[language]["ignore"]:
sort_translation_status[language]["ignore"].remove(default_key.strip())
except IndexError:
pass
print(f"{language}: {fails} out of {num_lines} lines are not translated.")
result_list.append(
(
language,
int((num_lines - fails) * 100 / num_lines),
)
)
translation_status = convert_to_multiline(sort_translation_status)
with open(translation_status_file, "w", encoding="utf-8") as file:
file.write(tomlkit.dumps(translation_status))
unique_data = list(set(result_list))
unique_data.sort(key=lambda x: x[1], reverse=True)
return unique_data
if __name__ == "__main__":
directory = os.path.join(os.getcwd(), "src", "main", "resources")
messages_file_paths = glob.glob(os.path.join(directory, "messages_*.properties"))
reference_file = os.path.join(directory, "messages_en_GB.properties")
scripts_directory = os.path.join(os.getcwd(), "scripts")
translation_state_file = os.path.join(scripts_directory, "translation_status.toml")
write_readme(compare_files(reference_file, messages_file_paths, translation_state_file))

View File

@@ -0,0 +1,37 @@
import cv2
import sys
import argparse
import numpy as np
def is_blank_image(image_path, threshold=10, white_percent=99, white_value=255, blur_size=5):
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
if image is None:
print(f"Error: Unable to read the image file: {image_path}")
return False
# Apply Gaussian blur to reduce noise
blurred_image = cv2.GaussianBlur(image, (blur_size, blur_size), 0)
_, thresholded_image = cv2.threshold(blurred_image, white_value - threshold, white_value, cv2.THRESH_BINARY)
# Calculate the percentage of white pixels in the thresholded image
white_pixels = np.sum(thresholded_image == white_value)
white_pixel_percentage = (white_pixels / thresholded_image.size) * 100
print(f"Page has white pixel percent of {white_pixel_percentage}")
return white_pixel_percentage >= white_percent
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Detect if an image is considered blank or not.')
parser.add_argument('image_path', help='The path to the image file.')
parser.add_argument('-t', '--threshold', type=int, default=10, help='Threshold for determining white pixels. The default value is 10.')
parser.add_argument('-w', '--white_percent', type=float, default=99, help='The percentage of white pixels for an image to be considered blank. The default value is 99.')
args = parser.parse_args()
blank = is_blank_image(args.image_path, args.threshold, args.white_percent)
# Return code 1: The image is considered blank.
# Return code 0: The image is not considered blank.
sys.exit(int(blank))

View File

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

View File

@@ -1,34 +1,6 @@
#!/bin/bash #!/bin/sh
# Update the user and group IDs as per environment variables
if [ ! -z "$PUID" ] && [ "$PUID" != "$(id -u stirlingpdfuser)" ]; then
usermod -o -u "$PUID" stirlingpdfuser || true
fi
if [ ! -z "$PGID" ] && [ "$PGID" != "$(getent group stirlingpdfgroup | cut -d: -f3)" ]; then
groupmod -o -g "$PGID" stirlingpdfgroup || true
fi
umask "$UMASK" || true
if [[ "$INSTALL_BOOK_AND_ADVANCED_HTML_OPS" == "true" ]]; then
apk add --no-cache calibre@testing
fi
/scripts/download-security-jar.sh /scripts/download-security-jar.sh
if [[ -n "$LANGS" ]]; then # Run the main command
/scripts/installFonts.sh $LANGS exec "$@"
fi
echo "Setting permissions and ownership for necessary directories..."
# Attempt to change ownership of directories and files
if chown -R stirlingpdfuser:stirlingpdfgroup $HOME /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar; then
chmod -R 755 /logs /scripts /usr/share/fonts/opentype/noto /configs /customFiles /pipeline /app.jar || true
# If chown succeeds, execute the command as stirlingpdfuser
exec su-exec stirlingpdfuser "$@"
else
# If chown fails, execute the command without changing the user context
echo "[WARN] Chown failed, running as host user"
exec "$@"
fi

View File

@@ -2,30 +2,25 @@
# Copy the original tesseract-ocr files to the volume directory without overwriting existing files # Copy the original tesseract-ocr files to the volume directory without overwriting existing files
echo "Copying original files without overwriting existing files" echo "Copying original files without overwriting existing files"
mkdir -p /usr/share/tessdata mkdir -p /usr/share/tesseract-ocr
cp -rn /usr/share/tessdata-original/* /usr/share/tessdata cp -rn /usr/share/tesseract-ocr-original/* /usr/share/tesseract-ocr
if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then if [ -d /usr/share/tesseract-ocr/4.00/tessdata ]; then
cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tessdata || true; cp -r /usr/share/tesseract-ocr/4.00/tessdata/* /usr/share/tesseract-ocr/5/tessdata/ || true;
fi
if [ -d /usr/share/tesseract-ocr/5/tessdata ]; then
cp -r /usr/share/tesseract-ocr/5/tessdata/* /usr/share/tessdata || true;
fi fi
# Check if TESSERACT_LANGS environment variable is set and is not empty # Check if TESSERACT_LANGS environment variable is set and is not empty
if [[ -n "$TESSERACT_LANGS" ]]; then if [[ -n "$TESSERACT_LANGS" ]]; then
# Convert comma-separated values to a space-separated list # Convert comma-separated values to a space-separated list
LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ') LANGS=$(echo $TESSERACT_LANGS | tr ',' ' ')
pattern='^[a-zA-Z]{2,4}(_[a-zA-Z]{2,4})?$'
# Install each language pack # Install each language pack
for LANG in $LANGS; do for LANG in $LANGS; do
if [[ $LANG =~ $pattern ]]; then apt-get install -y "tesseract-ocr-$LANG"
apk add --no-cache "tesseract-ocr-data-$LANG"
else
echo "Skipping invalid language code"
fi
done done
fi fi
/scripts/init-without-ocr.sh "$@" /scripts/download-security-jar.sh
# Run the main command
exec "$@"

View File

@@ -1,67 +0,0 @@
#!/bin/bash
LANGS=$1
# Function to install a font package
install_font() {
echo "Installing font package: $1"
if ! apk add "$1" --no-cache; then
echo "Failed to install $1"
fi
}
# Install common fonts used across many languages
#common_fonts=(
# font-terminus
# font-dejavu
# font-noto
# font-noto-cjk
# font-awesome
# font-noto-extra
#)
#
#for font in "${common_fonts[@]}"; do
# install_font $font
#done
# Map languages to specific font packages
declare -A language_fonts=(
["ar_AR"]="font-noto-arabic"
["zh_CN"]="font-isas-misc"
["zh_TW"]="font-isas-misc"
["ja_JP"]="font-noto font-noto-thai font-noto-tibetan font-ipa font-sony-misc font-jis-misc"
["ru_RU"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["sr_LATN_RS"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["uk_UA"]="font-vollkorn font-misc-cyrillic font-mutt-misc font-screen-cyrillic font-winitzki-cyrillic font-cronyx-cyrillic"
["ko_KR"]="font-noto font-noto-thai font-noto-tibetan"
["el_GR"]="font-noto"
["hi_IN"]="font-noto-devanagari"
["bg_BG"]="font-vollkorn font-misc-cyrillic"
["GENERAL"]="font-terminus font-dejavu font-noto font-noto-cjk font-awesome font-noto-extra"
)
# Install fonts for other languages which generally do not need special packages beyond 'font-noto'
other_langs=("en_GB" "en_US" "de_DE" "fr_FR" "es_ES" "ca_CA" "it_IT" "pt_BR" "nl_NL" "sv_SE" "pl_PL" "ro_RO" "hu_HU" "tr_TR" "id_ID" "eu_ES")
if [[ $LANGS == "ALL" ]]; then
# Install all fonts from the language_fonts map
for fonts in "${language_fonts[@]}"; do
for font in $fonts; do
install_font $font
done
done
else
# Split comma-separated languages and install necessary fonts
IFS=',' read -ra LANG_CODES <<< "$LANGS"
for code in "${LANG_CODES[@]}"; do
if [[ " ${other_langs[@]} " =~ " ${code} " ]]; then
install_font font-noto
else
fonts_to_install=${language_fonts[$code]}
if [ ! -z "$fonts_to_install" ]; then
for font in $fonts_to_install; do
install_font $font
done
fi
fi
done
fi

View File

@@ -2,7 +2,7 @@ import argparse
import sys import sys
import cv2 import cv2
import numpy as np import numpy as np
import os import os
def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500): def find_photo_boundaries(image, background_color, tolerance=30, min_area=10000, min_contour_area=500):
mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance) mask = cv2.inRange(image, background_color - tolerance, background_color + tolerance)
@@ -49,9 +49,9 @@ def auto_rotate(image, angle_threshold=1):
angles = [] angles = []
for rho, theta in lines[:, 0]: for rho, theta in lines[:, 0]:
angles.append((theta * 180) / np.pi - 90) angles.append((theta * 180) / np.pi - 90)
angle = np.median(angles) angle = np.median(angles)
if abs(angle) < angle_threshold: if abs(angle) < angle_threshold:
return image return image
@@ -65,16 +65,16 @@ def auto_rotate(image, angle_threshold=1):
def crop_borders(image, border_color, tolerance=30): def crop_borders(image, border_color, tolerance=30):
mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance) mask = cv2.inRange(image, border_color - tolerance, border_color + tolerance)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) == 0: if len(contours) == 0:
return image return image
largest_contour = max(contours, key=cv2.contourArea) largest_contour = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest_contour) x, y, w, h = cv2.boundingRect(largest_contour)
return image[y:y+h, x:x+w] return image[y:y+h, x:x+w]
def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0): def split_photos(input_file, output_directory, tolerance=30, min_area=10000, min_contour_area=500, angle_threshold=10, border_size=0):
image = cv2.imread(input_file) image = cv2.imread(input_file)
background_color = estimate_background_color(image) background_color = estimate_background_color(image)
@@ -110,7 +110,7 @@ if __name__ == "__main__":
parser.add_argument("--min_contour_area", type=int, default=500, help="Sets the minimum contour area threshold for a photo (default: 500).") parser.add_argument("--min_contour_area", type=int, default=500, help="Sets the minimum contour area threshold for a photo (default: 500).")
parser.add_argument("--angle_threshold", type=int, default=10, help="Sets the minimum absolute angle required for the image to be rotated (default: 10).") parser.add_argument("--angle_threshold", type=int, default=10, help="Sets the minimum absolute angle required for the image to be rotated (default: 10).")
parser.add_argument("--border_size", type=int, default=0, help="Sets the size of the border added and removed to prevent white borders in the output (default: 0).") parser.add_argument("--border_size", type=int, default=0, help="Sets the size of the border added and removed to prevent white borders in the output (default: 0).")
args = parser.parse_args() args = parser.parse_args()
split_photos(args.input_file, args.output_directory, tolerance=args.tolerance, min_area=args.min_area, min_contour_area=args.min_contour_area, angle_threshold=args.angle_threshold, border_size=args.border_size) split_photos(args.input_file, args.output_directory, tolerance=args.tolerance, min_area=args.min_area, min_contour_area=args.min_contour_area, angle_threshold=args.angle_threshold, border_size=args.border_size)

View File

@@ -1,159 +0,0 @@
[ar_AR]
ignore = [
'language.direction',
]
[bg_BG]
ignore = [
'language.direction',
]
[ca_CA]
ignore = [
'language.direction',
]
[de_DE]
ignore = [
'AddStampRequest.alphabet',
'AddStampRequest.position',
'PDFToBook.selectText.1',
'PDFToText.tags',
'addPageNumbers.selectText.3',
'alphabet',
'certSign.name',
'language.direction',
'licenses.version',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'sponsor',
'text',
'watermark.type.1',
]
[el_GR]
ignore = [
'language.direction',
]
[es_ES]
ignore = [
'adminUserSettings.roles',
'color',
'language.direction',
'no',
'showJS.tags',
]
[eu_ES]
ignore = [
'language.direction',
]
[fr_FR]
ignore = [
'language.direction',
]
[hi_IN]
ignore = [
'language.direction',
]
[hu_HU]
ignore = [
'language.direction',
]
[id_ID]
ignore = [
'language.direction',
]
[it_IT]
ignore = [
'font',
'language.direction',
'no',
'password',
'pipeline.title',
'pipelineOptions.pipelineHeader',
'removePassword.selectText.2',
'showJS.tags',
'sponsor',
]
[ja_JP]
ignore = [
'language.direction',
]
[ko_KR]
ignore = [
'language.direction',
]
[nl_NL]
ignore = [
'language.direction',
]
[pl_PL]
ignore = [
'language.direction',
]
[pt_BR]
ignore = [
'language.direction',
]
[pt_PT]
ignore = [
'language.direction',
]
[ro_RO]
ignore = [
'language.direction',
]
[ru_RU]
ignore = [
'language.direction',
]
[sk_SK]
ignore = [
'language.direction',
]
[sr_LATN_RS]
ignore = [
'language.direction',
]
[sv_SE]
ignore = [
'language.direction',
]
[tr_TR]
ignore = [
'language.direction',
]
[uk_UA]
ignore = [
'language.direction',
]
[zh_CN]
ignore = [
'language.direction',
]
[zh_TW]
ignore = [
'language.direction',
]

View File

@@ -1,64 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.cms.CMSObjectIdentifiers;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSTypedData;
/**
* Wraps a InputStream into a CMSProcessable object for bouncy castle. It's a memory saving
* alternative to the {@link org.bouncycastle.cms.CMSProcessableByteArray CMSProcessableByteArray}
* class.
*
* @author Thomas Chojecki
*/
class CMSProcessableInputStream implements CMSTypedData {
private final InputStream in;
private final ASN1ObjectIdentifier contentType;
CMSProcessableInputStream(InputStream is) {
this(new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId()), is);
}
CMSProcessableInputStream(ASN1ObjectIdentifier type, InputStream is) {
contentType = type;
in = is;
}
@Override
public Object getContent() {
return in;
}
@Override
public void write(OutputStream out) throws IOException, CMSException {
// read the content only one time
in.transferTo(out);
in.close();
}
@Override
public ASN1ObjectIdentifier getContentType() {
return contentType;
}
}

View File

@@ -1,170 +0,0 @@
/*
* Copyright 2015 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.SignatureInterface;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
public abstract class CreateSignatureBase implements SignatureInterface {
private PrivateKey privateKey;
private Certificate[] certificateChain;
private String tsaUrl;
private boolean externalSigning;
/**
* Initialize the signature creator with a keystore (pkcs12) and pin that should be used for the
* signature.
*
* @param keystore is a pkcs12 keystore.
* @param pin is the pin for the keystore / private key
* @throws KeyStoreException if the keystore has not been initialized (loaded)
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found
* @throws UnrecoverableKeyException if the given password is wrong
* @throws CertificateException if the certificate is not valid as signing time
* @throws IOException if no certificate could be found
*/
public CreateSignatureBase(KeyStore keystore, char[] pin)
throws KeyStoreException,
UnrecoverableKeyException,
NoSuchAlgorithmException,
IOException,
CertificateException {
// grabs the first alias from the keystore and get the private key. An
// alternative method or constructor could be used for setting a specific
// alias that should be used.
Enumeration<String> aliases = keystore.aliases();
String alias;
Certificate cert = null;
while (cert == null && aliases.hasMoreElements()) {
alias = aliases.nextElement();
setPrivateKey((PrivateKey) keystore.getKey(alias, pin));
Certificate[] certChain = keystore.getCertificateChain(alias);
if (certChain != null) {
setCertificateChain(certChain);
cert = certChain[0];
if (cert instanceof X509Certificate) {
// avoid expired certificate
((X509Certificate) cert).checkValidity();
//// SigUtils.checkCertificateUsage((X509Certificate) cert);
}
}
}
if (cert == null) {
throw new IOException("Could not find certificate");
}
}
public final void setPrivateKey(PrivateKey privateKey) {
this.privateKey = privateKey;
}
public final void setCertificateChain(final Certificate[] certificateChain) {
this.certificateChain = certificateChain;
}
public Certificate[] getCertificateChain() {
return certificateChain;
}
public void setTsaUrl(String tsaUrl) {
this.tsaUrl = tsaUrl;
}
/**
* SignatureInterface sample implementation.
*
* <p>This method will be called from inside of the pdfbox and create the PKCS #7 signature. The
* given InputStream contains the bytes that are given by the byte range.
*
* <p>This method is for internal use only.
*
* <p>Use your favorite cryptographic library to implement PKCS #7 signature creation. If you
* want to create the hash and the signature separately (e.g. to transfer only the hash to an
* external application), read <a href="https://stackoverflow.com/questions/41767351">this
* answer</a> or <a href="https://stackoverflow.com/questions/56867465">this answer</a>.
*
* @throws IOException
*/
@Override
public byte[] sign(InputStream content) throws IOException {
// cannot be done private (interface)
try {
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
X509Certificate cert = (X509Certificate) certificateChain[0];
ContentSigner sha1Signer =
new JcaContentSignerBuilder("SHA256WithRSA").build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder().build())
.build(sha1Signer, cert));
gen.addCertificates(new JcaCertStore(Arrays.asList(certificateChain)));
CMSProcessableInputStream msg = new CMSProcessableInputStream(content);
CMSSignedData signedData = gen.generate(msg, false);
if (tsaUrl != null && !tsaUrl.isEmpty()) {
ValidationTimeStamp validation = new ValidationTimeStamp(tsaUrl);
signedData = validation.addSignedTimeStamp(signedData);
}
return signedData.getEncoded();
} catch (GeneralSecurityException
| CMSException
| OperatorCreationException
| URISyntaxException e) {
throw new IOException(e);
}
}
/**
* Set if external signing scenario should be used. If {@code false}, SignatureInterface would
* be used for signing.
*
* <p>Default: {@code false}
*
* @param externalSigning {@code true} if external signing should be performed
*/
public void setExternalSigning(boolean externalSigning) {
this.externalSigning = externalSigning;
}
public boolean isExternalSigning() {
return externalSigning;
}
}

View File

@@ -1,176 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
import org.bouncycastle.tsp.TSPException;
import org.bouncycastle.tsp.TimeStampRequest;
import org.bouncycastle.tsp.TimeStampRequestGenerator;
import org.bouncycastle.tsp.TimeStampResponse;
import org.bouncycastle.tsp.TimeStampToken;
/**
* Time Stamping Authority (TSA) Client [RFC 3161].
*
* @author Vakhtang Koroghlishvili
* @author John Hewson
*/
public class TSAClient {
private static final Logger LOG = LogManager.getLogger(TSAClient.class);
private static final DigestAlgorithmIdentifierFinder ALGORITHM_OID_FINDER =
new DefaultDigestAlgorithmIdentifierFinder();
private final URL url;
private final String username;
private final String password;
private final MessageDigest digest;
// SecureRandom.getInstanceStrong() would be better, but sometimes blocks on Linux
private static final Random RANDOM = new SecureRandom();
/**
* @param url the URL of the TSA service
* @param username user name of TSA
* @param password password of TSA
* @param digest the message digest to use
*/
public TSAClient(URL url, String username, String password, MessageDigest digest) {
this.url = url;
this.username = username;
this.password = password;
this.digest = digest;
}
/**
* @param content
* @return the time stamp token
* @throws IOException if there was an error with the connection or data from the TSA server, or
* if the time stamp response could not be validated
*/
public TimeStampToken getTimeStampToken(InputStream content) throws IOException {
digest.reset();
DigestInputStream dis = new DigestInputStream(content, digest);
while (dis.read() != -1) {
// do nothing
}
byte[] hash = digest.digest();
// 32-bit cryptographic nonce
int nonce = RANDOM.nextInt();
// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = ALGORITHM_OID_FINDER.find(digest.getAlgorithm()).getAlgorithm();
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));
// get TSA response
byte[] tsaResponse = getTSAResponse(request.getEncoded());
TimeStampResponse response;
try {
response = new TimeStampResponse(tsaResponse);
response.validate(request);
} catch (TSPException e) {
throw new IOException(e);
}
TimeStampToken timeStampToken = response.getTimeStampToken();
if (timeStampToken == null) {
// https://www.ietf.org/rfc/rfc3161.html#section-2.4.2
throw new IOException(
"Response from "
+ url
+ " does not have a time stamp token, status: "
+ response.getStatus()
+ " ("
+ response.getStatusString()
+ ")");
}
return timeStampToken;
}
// gets response data for the given encoded TimeStampRequest data
// throws IOException if a connection to the TSA cannot be established
private byte[] getTSAResponse(byte[] request) throws IOException {
LOG.debug("Opening connection to TSA server");
// todo: support proxy servers
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestProperty("Content-Type", "application/timestamp-query");
LOG.debug("Established connection to TSA server");
if (username != null && password != null && !username.isEmpty() && !password.isEmpty()) {
String contentEncoding = connection.getContentEncoding();
if (contentEncoding == null) {
contentEncoding = StandardCharsets.UTF_8.name();
}
connection.setRequestProperty(
"Authorization",
"Basic "
+ new String(
Base64.getEncoder()
.encode(
(username + ":" + password)
.getBytes(contentEncoding))));
}
// read response
try (OutputStream output = connection.getOutputStream()) {
output.write(request);
} catch (IOException ex) {
LOG.error("Exception when writing to {}", this.url, ex);
throw ex;
}
LOG.debug("Waiting for response from TSA server");
byte[] response;
try (InputStream input = connection.getInputStream()) {
response = input.readAllBytes();
} catch (IOException ex) {
LOG.error("Exception when reading from {}", this.url, ex);
throw ex;
}
LOG.debug("Received response from TSA server");
return response;
}
}

View File

@@ -1,134 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.signature;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.Attributes;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationStore;
import org.bouncycastle.tsp.TimeStampToken;
/**
* This class wraps the TSAClient and the work that has to be done with it. Like Adding Signed
* TimeStamps to a signature, or creating a CMS timestamp attribute (with a signed timestamp)
*
* @author Others
* @author Alexis Suter
*/
public class ValidationTimeStamp {
private TSAClient tsaClient;
/**
* @param tsaUrl The url where TS-Request will be done.
* @throws NoSuchAlgorithmException
* @throws MalformedURLException
* @throws java.net.URISyntaxException
*/
public ValidationTimeStamp(String tsaUrl)
throws NoSuchAlgorithmException, MalformedURLException, URISyntaxException {
if (tsaUrl != null) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
this.tsaClient = new TSAClient(new URI(tsaUrl).toURL(), null, null, digest);
}
}
/**
* Creates a signed timestamp token by the given input stream.
*
* @param content InputStream of the content to sign
* @return the byte[] of the timestamp token
* @throws IOException
*/
public byte[] getTimeStampToken(InputStream content) throws IOException {
TimeStampToken timeStampToken = tsaClient.getTimeStampToken(content);
return timeStampToken.getEncoded();
}
/**
* Extend cms signed data with TimeStamp first or to all signers
*
* @param signedData Generated CMS signed data
* @return CMSSignedData Extended CMS signed data
* @throws IOException
*/
public CMSSignedData addSignedTimeStamp(CMSSignedData signedData) throws IOException {
SignerInformationStore signerStore = signedData.getSignerInfos();
List<SignerInformation> newSigners = new ArrayList<>();
for (SignerInformation signer : signerStore.getSigners()) {
// This adds a timestamp to every signer (into his unsigned attributes) in the
// signature.
newSigners.add(signTimeStamp(signer));
}
// Because new SignerInformation is created, new SignerInfoStore has to be created
// and also be replaced in signedData. Which creates a new signedData object.
return CMSSignedData.replaceSigners(signedData, new SignerInformationStore(newSigners));
}
/**
* Extend CMS Signer Information with the TimeStampToken into the unsigned Attributes.
*
* @param signer information about signer
* @return information about SignerInformation
* @throws IOException
*/
private SignerInformation signTimeStamp(SignerInformation signer) throws IOException {
AttributeTable unsignedAttributes = signer.getUnsignedAttributes();
ASN1EncodableVector vector = new ASN1EncodableVector();
if (unsignedAttributes != null) {
vector = unsignedAttributes.toASN1EncodableVector();
}
TimeStampToken timeStampToken =
tsaClient.getTimeStampToken(new ByteArrayInputStream(signer.getSignature()));
byte[] token = timeStampToken.getEncoded();
ASN1ObjectIdentifier oid = PKCSObjectIdentifiers.id_aa_signatureTimeStampToken;
ASN1Encodable signatureTimeStamp =
new Attribute(oid, new DERSet(ASN1Primitive.fromByteArray(token)));
vector.add(signatureTimeStamp);
Attributes signedAttributes = new Attributes(vector);
// There is no other way changing the unsigned attributes of the signer information.
// result is never null, new SignerInformation always returned,
// see source code of replaceUnsignedAttributes
return SignerInformation.replaceUnsignedAttributes(
signer, new AttributeTable(signedAttributes));
}
}

View File

@@ -1,82 +0,0 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.examples.util;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
/**
* Delegate class to close the connection when the class gets closed.
*
* @author Tilman Hausherr
*/
public class ConnectedInputStream extends InputStream {
HttpURLConnection con;
InputStream is;
public ConnectedInputStream(HttpURLConnection con, InputStream is) {
this.con = con;
this.is = is;
}
@Override
public int read() throws IOException {
return is.read();
}
@Override
public int read(byte[] b) throws IOException {
return is.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return is.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return is.skip(n);
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public synchronized void mark(int readlimit) {
is.mark(readlimit);
}
@Override
public synchronized void reset() throws IOException {
is.reset();
}
@Override
public boolean markSupported() {
return is.markSupported();
}
@Override
public void close() throws IOException {
is.close();
con.disconnect();
}
}

View File

@@ -6,8 +6,6 @@ import java.net.Socket;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import io.github.pixee.security.SystemCommand;
public class LibreOfficeListener { public class LibreOfficeListener {
private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes private static final long ACTIVITY_TIMEOUT = 20 * 60 * 1000; // 20 minutes
@@ -46,7 +44,7 @@ public class LibreOfficeListener {
} }
// Start the listener process // Start the listener process
process = SystemCommand.runCommand(Runtime.getRuntime(), "unoconv --listener"); process = Runtime.getRuntime().exec("unoconv --listener");
lastActivityTime = System.currentTimeMillis(); lastActivityTime = System.currentTimeMillis();
// Start a background thread to monitor the activity timeout // Start a background thread to monitor the activity timeout

View File

@@ -1,131 +1,81 @@
package stirling.software.SPDF; package stirling.software.SPDF;
import java.io.IOException; import java.nio.file.Files;
import java.nio.file.Files; import java.nio.file.Paths;
import java.nio.file.Path; import java.util.Collections;
import java.nio.file.Paths;
import java.util.Collections; import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashMap; import org.springframework.boot.SpringApplication;
import java.util.Map; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment;
import org.slf4j.Logger; import org.springframework.scheduling.annotation.EnableScheduling;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value; import stirling.software.SPDF.config.ConfigInitializer;
import org.springframework.boot.SpringApplication; import stirling.software.SPDF.utils.GeneralUtils;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.env.Environment; @SpringBootApplication
import org.springframework.scheduling.annotation.EnableScheduling; @EnableScheduling
public class SPdfApplication {
import io.github.pixee.security.SystemCommand;
@Autowired private Environment env;
import jakarta.annotation.PostConstruct;
import stirling.software.SPDF.config.ConfigInitializer; @PostConstruct
import stirling.software.SPDF.model.ApplicationProperties; public void init() {
// Check if the BROWSER_OPEN environment variable is set to true
@SpringBootApplication String browserOpenEnv = env.getProperty("BROWSER_OPEN");
@EnableScheduling boolean browserOpen = browserOpenEnv != null && browserOpenEnv.equalsIgnoreCase("true");
public class SPdfApplication {
if (browserOpen) {
private static final Logger logger = LoggerFactory.getLogger(SPdfApplication.class); try {
String url = "http://localhost:" + getPort();
@Autowired private Environment env;
String os = System.getProperty("os.name").toLowerCase();
@Autowired ApplicationProperties applicationProperties; Runtime rt = Runtime.getRuntime();
if (os.contains("win")) {
private static String serverPortStatic; // For Windows
rt.exec("rundll32 url.dll,FileProtocolHandler " + url);
@Value("${server.port:8080}") }
public void setServerPortStatic(String port) { } catch (Exception e) {
SPdfApplication.serverPortStatic = port; e.printStackTrace();
} }
}
@PostConstruct }
public void init() {
// Check if the BROWSER_OPEN environment variable is set to true public static void main(String[] args) {
String browserOpenEnv = env.getProperty("BROWSER_OPEN"); SpringApplication app = new SpringApplication(SPdfApplication.class);
boolean browserOpen = browserOpenEnv != null && "true".equalsIgnoreCase(browserOpenEnv); app.addInitializers(new ConfigInitializer());
if (Files.exists(Paths.get("configs/settings.yml"))) {
if (browserOpen) { app.setDefaultProperties(
try { Collections.singletonMap(
String url = "http://localhost:" + getNonStaticPort(); "spring.config.additional-location", "file:configs/settings.yml"));
} else {
String os = System.getProperty("os.name").toLowerCase(); System.out.println(
Runtime rt = Runtime.getRuntime(); "External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead.");
if (os.contains("win")) { }
// For Windows app.run(args);
SystemCommand.runCommand(rt, "rundll32 url.dll,FileProtocolHandler " + url);
} try {
} catch (Exception e) { Thread.sleep(1000);
logger.error("Error opening browser: {}", e.getMessage()); } catch (InterruptedException e) {
} // TODO Auto-generated catch block
} e.printStackTrace();
logger.info("Running configs {}", applicationProperties.toString()); }
}
GeneralUtils.createDir("customFiles/static/");
public static void main(String[] args) throws IOException, InterruptedException { GeneralUtils.createDir("customFiles/templates/");
SpringApplication app = new SpringApplication(SPdfApplication.class); System.out.println("Stirling-PDF Started.");
app.addInitializers(new ConfigInitializer());
Map<String, String> propertyFiles = new HashMap<>(); String url = "http://localhost:" + getPort();
System.out.println("Navigate to " + url);
// stirling pdf settings file }
if (Files.exists(Paths.get("configs/settings.yml"))) {
propertyFiles.put("spring.config.additional-location", "file:configs/settings.yml"); public static String getPort() {
} else { String port = System.getProperty("local.server.port");
logger.warn( if (port == null || port.isEmpty()) {
"External configuration file 'configs/settings.yml' does not exist. Using default configuration and environment configuration instead."); port = "8080";
} }
return port;
// custom javs settings file }
if (Files.exists(Paths.get("configs/custom_settings.yml"))) { }
String existing = propertyFiles.getOrDefault("spring.config.additional-location", "");
if (!existing.isEmpty()) {
existing += ",";
}
propertyFiles.put(
"spring.config.additional-location",
existing + "file:configs/custom_settings.yml");
} else {
logger.warn("Custom configuration file 'configs/custom_settings.yml' does not exist.");
}
if (!propertyFiles.isEmpty()) {
app.setDefaultProperties(
Collections.singletonMap(
"spring.config.additional-location",
propertyFiles.get("spring.config.additional-location")));
}
app.run(args);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted while sleeping", e);
}
try {
Files.createDirectories(Path.of("customFiles/static/"));
Files.createDirectories(Path.of("customFiles/templates/"));
} catch (Exception e) {
logger.error("Error creating directories: {}", e.getMessage());
}
printStartupLogs();
}
private static void printStartupLogs() {
logger.info("Stirling-PDF Started.");
String url = "http://localhost:" + getStaticPort();
logger.info("Navigate to {}", url);
}
public static String getStaticPort() {
return serverPortStatic;
}
public String getNonStaticPort() {
return serverPortStatic;
}
}

View File

@@ -1,111 +1,60 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired;
import java.nio.file.Files; import org.springframework.context.annotation.Bean;
import java.nio.file.Paths; import org.springframework.context.annotation.Configuration;
import java.util.Properties;
import stirling.software.SPDF.model.ApplicationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; @Configuration
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; public class AppConfig {
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Autowired ApplicationProperties applicationProperties;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.io.ClassPathResource; @Bean(name = "loginEnabled")
import org.springframework.core.io.Resource; public boolean loginEnabled() {
import org.springframework.core.io.ResourceLoader; return applicationProperties.getSecurity().getEnableLogin();
import org.thymeleaf.spring6.SpringTemplateEngine; }
import stirling.software.SPDF.model.ApplicationProperties; @Bean(name = "appName")
public String appName() {
@Configuration String homeTitle = applicationProperties.getUi().getAppName();
@Lazy return (homeTitle != null) ? homeTitle : "Stirling PDF";
public class AppConfig { }
@Autowired ApplicationProperties applicationProperties; @Bean(name = "appVersion")
public String appVersion() {
@Bean String version = getClass().getPackage().getImplementationVersion();
@ConditionalOnProperty( return (version != null) ? version : "0.0.0";
name = "system.customHTMLFiles", }
havingValue = "true",
matchIfMissing = false) @Bean(name = "homeText")
public SpringTemplateEngine templateEngine(ResourceLoader resourceLoader) { public String homeText() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine(); return (applicationProperties.getUi().getHomeDescription() != null)
templateEngine.addTemplateResolver(new FileFallbackTemplateResolver(resourceLoader)); ? applicationProperties.getUi().getHomeDescription()
return templateEngine; : "null";
} }
@Bean(name = "loginEnabled") @Bean(name = "navBarText")
public boolean loginEnabled() { public String navBarText() {
return applicationProperties.getSecurity().getEnableLogin(); String defaultNavBar =
} applicationProperties.getUi().getAppNameNavbar() != null
? applicationProperties.getUi().getAppNameNavbar()
@Bean(name = "appName") : applicationProperties.getUi().getAppName();
public String appName() { return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
String homeTitle = applicationProperties.getUi().getAppName(); }
return (homeTitle != null) ? homeTitle : "Stirling PDF";
} @Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() {
@Bean(name = "appVersion") return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
public String appVersion() { ? applicationProperties.getSystem().getEnableAlphaFunctionality()
Resource resource = new ClassPathResource("version.properties"); : false;
Properties props = new Properties(); }
try {
props.load(resource.getInputStream()); @Bean(name = "rateLimit")
return props.getProperty("version"); public boolean rateLimit() {
} catch (IOException e) { String appName = System.getProperty("rateLimit");
e.printStackTrace(); if (appName == null) appName = System.getenv("rateLimit");
} return (appName != null) ? Boolean.valueOf(appName) : false;
return "0.0.0"; }
} }
@Bean(name = "homeText")
public String homeText() {
return (applicationProperties.getUi().getHomeDescription() != null)
? applicationProperties.getUi().getHomeDescription()
: "null";
}
@Bean(name = "navBarText")
public String navBarText() {
String defaultNavBar =
applicationProperties.getUi().getAppNameNavbar() != null
? applicationProperties.getUi().getAppNameNavbar()
: applicationProperties.getUi().getAppName();
return (defaultNavBar != null) ? defaultNavBar : "Stirling PDF";
}
@Bean(name = "enableAlphaFunctionality")
public boolean enableAlphaFunctionality() {
return applicationProperties.getSystem().getEnableAlphaFunctionality() != null
? applicationProperties.getSystem().getEnableAlphaFunctionality()
: false;
}
@Bean(name = "rateLimit")
public boolean rateLimit() {
String appName = System.getProperty("rateLimit");
if (appName == null) appName = System.getenv("rateLimit");
return (appName != null) ? Boolean.valueOf(appName) : false;
}
@Bean(name = "RunningInDocker")
public boolean runningInDocker() {
return Files.exists(Paths.get("/.dockerenv"));
}
@Bean(name = "bookAndHtmlFormatsInstalled")
public boolean bookAndHtmlFormatsInstalled() {
String installOps = System.getProperty("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
if (installOps == null) {
installOps = System.getenv("INSTALL_BOOK_AND_ADVANCED_HTML_OPS");
}
return "true".equalsIgnoreCase(installOps);
}
@ConditionalOnMissingClass("stirling.software.SPDF.config.security.SecurityConfiguration")
@Bean(name = "activSecurity")
public boolean missingActivSecurity() {
return false;
}
}

View File

@@ -1,25 +0,0 @@
package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.ApplicationProperties;
@Service
class AppUpdateService {
@Autowired private ApplicationProperties applicationProperties;
@Autowired(required = false)
ShowAdminInterface showAdmin;
@Bean(name = "shouldShow")
@Scope("request")
public boolean shouldShow() {
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
boolean showAdminResult = (showAdmin != null) ? showAdmin.getShowUpdateOnlyAdmins() : true;
return showUpdate && showAdminResult;
}
}

View File

@@ -1,64 +1,64 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Locale; import java.util.Locale;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver; import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class Beans implements WebMvcConfigurer { public class Beans implements WebMvcConfigurer {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor()); registry.addInterceptor(localeChangeInterceptor());
registry.addInterceptor(new CleanUrlInterceptor()); registry.addInterceptor(new CleanUrlInterceptor());
} }
@Bean @Bean
public LocaleChangeInterceptor localeChangeInterceptor() { public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor lci = new LocaleChangeInterceptor(); LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName("lang"); lci.setParamName("lang");
return lci; return lci;
} }
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
SessionLocaleResolver slr = new SessionLocaleResolver(); SessionLocaleResolver slr = new SessionLocaleResolver();
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale(); String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
Locale defaultLocale = Locale defaultLocale =
Locale.UK; // Fallback to UK locale if environment variable is not set Locale.UK; // Fallback to UK locale if environment variable is not set
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) { if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv); Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
String tempLanguageTag = tempLocale.toLanguageTag(); String tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-")); tempLocale = Locale.forLanguageTag(appLocaleEnv.replace("_", "-"));
tempLanguageTag = tempLocale.toLanguageTag(); tempLanguageTag = tempLocale.toLanguageTag();
if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) { if (appLocaleEnv.equalsIgnoreCase(tempLanguageTag)) {
defaultLocale = tempLocale; defaultLocale = tempLocale;
} else { } else {
System.err.println( System.err.println(
"Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK."); "Invalid APP_LOCALE environment variable value. Falling back to default Locale.UK.");
} }
} }
} }
slr.setDefaultLocale(defaultLocale); slr.setDefaultLocale(defaultLocale);
return slr; return slr;
} }
} }

View File

@@ -1,81 +1,74 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
public class CleanUrlInterceptor implements HandlerInterceptor { public class CleanUrlInterceptor implements HandlerInterceptor {
private static final List<String> ALLOWED_PARAMS = private static final List<String> ALLOWED_PARAMS =
Arrays.asList( Arrays.asList(
"lang", "lang", "endpoint", "endpoints", "logout", "error", "file", "messageType");
"endpoint",
"endpoints", @Override
"logout", public boolean preHandle(
"error", HttpServletRequest request, HttpServletResponse response, Object handler)
"erroroauth", throws Exception {
"file", String queryString = request.getQueryString();
"messageType"); if (queryString != null && !queryString.isEmpty()) {
String requestURI = request.getRequestURI();
@Override Map<String, String> parameters = new HashMap<>();
public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) // Keep only the allowed parameters
throws Exception { String[] queryParameters = queryString.split("&");
String queryString = request.getQueryString(); for (String param : queryParameters) {
if (queryString != null && !queryString.isEmpty()) { String[] keyValue = param.split("=");
String requestURI = request.getRequestURI(); if (keyValue.length != 2) {
Map<String, String> parameters = new HashMap<>(); continue;
}
// Keep only the allowed parameters if (ALLOWED_PARAMS.contains(keyValue[0])) {
String[] queryParameters = queryString.split("&"); parameters.put(keyValue[0], keyValue[1]);
for (String param : queryParameters) { }
String[] keyValue = param.split("="); }
if (keyValue.length != 2) {
continue; // If there are any parameters that are not allowed
} if (parameters.size() != queryParameters.length) {
if (ALLOWED_PARAMS.contains(keyValue[0])) { // Construct new query string
parameters.put(keyValue[0], keyValue[1]); StringBuilder newQueryString = new StringBuilder();
} for (Map.Entry<String, String> entry : parameters.entrySet()) {
} if (newQueryString.length() > 0) {
newQueryString.append("&");
// If there are any parameters that are not allowed }
if (parameters.size() != queryParameters.length) { newQueryString.append(entry.getKey()).append("=").append(entry.getValue());
// Construct new query string }
StringBuilder newQueryString = new StringBuilder();
for (Map.Entry<String, String> entry : parameters.entrySet()) { // Redirect to the URL with only allowed query parameters
if (newQueryString.length() > 0) { String redirectUrl = requestURI + "?" + newQueryString;
newQueryString.append("&"); response.sendRedirect(redirectUrl);
} return false;
newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); }
} }
return true;
// Redirect to the URL with only allowed query parameters }
String redirectUrl = requestURI + "?" + newQueryString;
response.sendRedirect(redirectUrl); @Override
return false; public void postHandle(
} HttpServletRequest request,
} HttpServletResponse response,
return true; Object handler,
} ModelAndView modelAndView) {}
@Override @Override
public void postHandle( public void afterCompletion(
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
Object handler, Object handler,
ModelAndView modelAndView) {} Exception ex) {}
}
@Override
public void afterCompletion(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {}
}

View File

@@ -1,18 +1,20 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.BufferedReader;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.URISyntaxException; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
@@ -24,12 +26,12 @@ public class ConfigInitializer
public void initialize(ConfigurableApplicationContext applicationContext) { public void initialize(ConfigurableApplicationContext applicationContext) {
try { try {
ensureConfigExists(); ensureConfigExists();
} catch (Exception e) { } catch (IOException e) {
throw new RuntimeException("Failed to initialize application configuration", e); throw new RuntimeException("Failed to initialize application configuration", e);
} }
} }
public void ensureConfigExists() throws IOException, URISyntaxException { public void ensureConfigExists() throws IOException {
// Define the path to the external config directory // Define the path to the external config directory
Path destPath = Paths.get("configs", "settings.yml"); Path destPath = Paths.get("configs", "settings.yml");
@@ -49,88 +51,93 @@ public class ConfigInitializer
} }
} }
} else { } else {
Path templatePath = // If user file exists, we need to merge it with the template from the classpath
Paths.get( List<String> templateLines;
getClass() try (InputStream in =
.getClassLoader() getClass().getClassLoader().getResourceAsStream("settings.yml.template")) {
.getResource("settings.yml.template") templateLines =
.toURI()); new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
Path userPath = Paths.get("configs", "settings.yml"); .lines()
.collect(Collectors.toList());
List<String> templateLines = Files.readAllLines(templatePath);
List<String> userLines =
Files.exists(userPath) ? Files.readAllLines(userPath) : new ArrayList<>();
List<String> resultLines = new ArrayList<>();
for (String templateLine : templateLines) {
// Check if the line is a comment
if (templateLine.trim().startsWith("#")) {
String entry = templateLine.trim().substring(1).trim();
if (!entry.isEmpty()) {
// Check if this comment has been uncommented in userLines
String key = entry.split(":")[0].trim();
addLine(resultLines, userLines, templateLine, key);
} else {
resultLines.add(templateLine);
}
}
// Check if the line is a key-value pair
else if (templateLine.contains(":")) {
String key = templateLine.split(":")[0].trim();
addLine(resultLines, userLines, templateLine, key);
}
// Handle empty lines
else if (templateLine.trim().length() == 0) {
resultLines.add("");
}
} }
// Write the result to the user settings file
Files.write(userPath, resultLines);
}
Path customSettingsPath = Paths.get("configs", "custom_settings.yml"); mergeYamlFiles(templateLines, destPath, destPath);
if (!Files.exists(customSettingsPath)) {
Files.createFile(customSettingsPath);
} }
} }
//TODO check parent value instead of just indent lines for duplicate keys (like enabled etc)
private static void addLine(List<String> resultLines, List<String> userLines, String templateLine, String key) {
boolean added = false;
int templateIndentationLevel = getIndentationLevel(templateLine);
for (String settingsLine : userLines) {
if (settingsLine.trim().startsWith(key + ":")) {
int settingsIndentationLevel = getIndentationLevel(settingsLine);
// Check if it is correct settingsLine and has the same parent as templateLine
if (settingsIndentationLevel == templateIndentationLevel) {
resultLines.add(settingsLine);
added = true;
break;
}
}
}
if (!added) {
resultLines.add(templateLine);
}
}
private static int getIndentationLevel(String line) { public void mergeYamlFiles(List<String> templateLines, Path userFilePath, Path outputPath)
int indentationLevel = 0; throws IOException {
String trimmedLine = line.trim(); List<String> userLines = Files.readAllLines(userFilePath);
if (trimmedLine.startsWith("#")) { List<String> mergedLines = new ArrayList<>();
line = trimmedLine.substring(1); boolean insideAutoGenerated = false;
} boolean beforeFirstKey = true;
for (char c : line.toCharArray()) {
if (c == ' ') { Function<String, Boolean> isCommented = line -> line.trim().startsWith("#");
indentationLevel++; Function<String, String> extractKey =
} else { line -> {
break; String[] parts = line.split(":");
return parts.length > 0 ? parts[0].trim().replace("#", "").trim() : "";
};
Set<String> userKeys = userLines.stream().map(extractKey).collect(Collectors.toSet());
for (String line : templateLines) {
String key = extractKey.apply(line);
if (line.trim().equalsIgnoreCase("AutomaticallyGenerated:")) {
insideAutoGenerated = true;
mergedLines.add(line);
continue;
} else if (insideAutoGenerated && line.trim().isEmpty()) {
insideAutoGenerated = false;
mergedLines.add(line);
continue;
}
if (beforeFirstKey && (isCommented.apply(line) || line.trim().isEmpty())) {
// Handle top comments and empty lines before the first key.
mergedLines.add(line);
continue;
}
if (!key.isEmpty()) beforeFirstKey = false;
if (userKeys.contains(key)) {
// If user has any version (commented or uncommented) of this key, skip the
// template line
Optional<String> userValue =
userLines.stream()
.filter(
l ->
extractKey.apply(l).equalsIgnoreCase(key)
&& !isCommented.apply(l))
.findFirst();
if (userValue.isPresent()) mergedLines.add(userValue.get());
continue;
}
if (isCommented.apply(line) || line.trim().isEmpty() || !userKeys.contains(key)) {
mergedLines.add(
line); // If line is commented, empty or key not present in user's file,
// retain the
// template line
continue;
} }
} }
return indentationLevel;
// Add any additional uncommented user lines that are not present in the
// template
for (String userLine : userLines) {
String userKey = extractKey.apply(userLine);
boolean isPresentInTemplate =
templateLines.stream()
.map(extractKey)
.anyMatch(templateKey -> templateKey.equalsIgnoreCase(userKey));
if (!isPresentInTemplate && !isCommented.apply(userLine)) {
mergedLines.add(userLine);
}
}
Files.write(outputPath, mergedLines, StandardCharsets.UTF_8);
} }
} }

View File

@@ -1,250 +1,231 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Service; import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties; @Service
public class EndpointConfiguration {
@Service private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
@DependsOn({"bookAndHtmlFormatsInstalled"}) private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>();
public class EndpointConfiguration { private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(EndpointConfiguration.class);
private Map<String, Boolean> endpointStatuses = new ConcurrentHashMap<>(); private final ApplicationProperties applicationProperties;
private Map<String, Set<String>> endpointGroups = new ConcurrentHashMap<>();
@Autowired
private final ApplicationProperties applicationProperties; public EndpointConfiguration(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
private boolean bookAndHtmlFormatsInstalled; init();
processEnvironmentConfigs();
@Autowired }
public EndpointConfiguration(
ApplicationProperties applicationProperties, public void enableEndpoint(String endpoint) {
@Qualifier("bookAndHtmlFormatsInstalled") boolean bookAndHtmlFormatsInstalled) { endpointStatuses.put(endpoint, true);
this.applicationProperties = applicationProperties; }
this.bookAndHtmlFormatsInstalled = bookAndHtmlFormatsInstalled;
init(); public void disableEndpoint(String endpoint) {
processEnvironmentConfigs(); if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) {
} logger.info("Disabling {}", endpoint);
endpointStatuses.put(endpoint, false);
public void enableEndpoint(String endpoint) { }
endpointStatuses.put(endpoint, true); }
}
public boolean isEndpointEnabled(String endpoint) {
public void disableEndpoint(String endpoint) { if (endpoint.startsWith("/")) {
if (!endpointStatuses.containsKey(endpoint) || endpointStatuses.get(endpoint) != false) { endpoint = endpoint.substring(1);
logger.info("Disabling {}", endpoint); }
endpointStatuses.put(endpoint, false); return endpointStatuses.getOrDefault(endpoint, true);
} }
}
public void addEndpointToGroup(String group, String endpoint) {
public boolean isEndpointEnabled(String endpoint) { endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint);
if (endpoint.startsWith("/")) { }
endpoint = endpoint.substring(1);
} public void enableGroup(String group) {
return endpointStatuses.getOrDefault(endpoint, true); Set<String> endpoints = endpointGroups.get(group);
} if (endpoints != null) {
for (String endpoint : endpoints) {
public void addEndpointToGroup(String group, String endpoint) { enableEndpoint(endpoint);
endpointGroups.computeIfAbsent(group, k -> new HashSet<>()).add(endpoint); }
} }
}
public void enableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group); public void disableGroup(String group) {
if (endpoints != null) { Set<String> endpoints = endpointGroups.get(group);
for (String endpoint : endpoints) { if (endpoints != null) {
enableEndpoint(endpoint); for (String endpoint : endpoints) {
} disableEndpoint(endpoint);
} }
} }
}
public void disableGroup(String group) {
Set<String> endpoints = endpointGroups.get(group); public void init() {
if (endpoints != null) { // Adding endpoints to "PageOps" group
for (String endpoint : endpoints) { addEndpointToGroup("PageOps", "remove-pages");
disableEndpoint(endpoint); addEndpointToGroup("PageOps", "merge-pdfs");
} addEndpointToGroup("PageOps", "split-pdfs");
} addEndpointToGroup("PageOps", "pdf-organizer");
} addEndpointToGroup("PageOps", "rotate-pdf");
addEndpointToGroup("PageOps", "multi-page-layout");
public void init() { addEndpointToGroup("PageOps", "scale-pages");
// Adding endpoints to "PageOps" group addEndpointToGroup("PageOps", "adjust-contrast");
addEndpointToGroup("PageOps", "remove-pages"); addEndpointToGroup("PageOps", "crop");
addEndpointToGroup("PageOps", "merge-pdfs"); addEndpointToGroup("PageOps", "auto-split-pdf");
addEndpointToGroup("PageOps", "split-pdfs"); addEndpointToGroup("PageOps", "extract-page");
addEndpointToGroup("PageOps", "pdf-organizer"); addEndpointToGroup("PageOps", "pdf-to-single-page");
addEndpointToGroup("PageOps", "rotate-pdf"); addEndpointToGroup("PageOps", "split-by-size-or-count");
addEndpointToGroup("PageOps", "multi-page-layout"); addEndpointToGroup("PageOps", "overlay-pdf");
addEndpointToGroup("PageOps", "scale-pages"); addEndpointToGroup("PageOps", "split-pdf-by-sections");
addEndpointToGroup("PageOps", "adjust-contrast");
addEndpointToGroup("PageOps", "crop"); // Adding endpoints to "Convert" group
addEndpointToGroup("PageOps", "auto-split-pdf"); addEndpointToGroup("Convert", "pdf-to-img");
addEndpointToGroup("PageOps", "extract-page"); addEndpointToGroup("Convert", "img-to-pdf");
addEndpointToGroup("PageOps", "pdf-to-single-page"); addEndpointToGroup("Convert", "pdf-to-pdfa");
addEndpointToGroup("PageOps", "split-by-size-or-count"); addEndpointToGroup("Convert", "file-to-pdf");
addEndpointToGroup("PageOps", "overlay-pdf"); addEndpointToGroup("Convert", "xlsx-to-pdf");
addEndpointToGroup("PageOps", "split-pdf-by-sections"); addEndpointToGroup("Convert", "pdf-to-word");
addEndpointToGroup("Convert", "pdf-to-presentation");
// Adding endpoints to "Convert" group addEndpointToGroup("Convert", "pdf-to-text");
addEndpointToGroup("Convert", "pdf-to-img"); addEndpointToGroup("Convert", "pdf-to-html");
addEndpointToGroup("Convert", "img-to-pdf"); addEndpointToGroup("Convert", "pdf-to-xml");
addEndpointToGroup("Convert", "pdf-to-pdfa"); addEndpointToGroup("Convert", "html-to-pdf");
addEndpointToGroup("Convert", "file-to-pdf"); addEndpointToGroup("Convert", "url-to-pdf");
addEndpointToGroup("Convert", "xlsx-to-pdf"); addEndpointToGroup("Convert", "markdown-to-pdf");
addEndpointToGroup("Convert", "pdf-to-word"); addEndpointToGroup("Convert", "pdf-to-csv");
addEndpointToGroup("Convert", "pdf-to-presentation");
addEndpointToGroup("Convert", "pdf-to-text"); // Adding endpoints to "Security" group
addEndpointToGroup("Convert", "pdf-to-html"); addEndpointToGroup("Security", "add-password");
addEndpointToGroup("Convert", "pdf-to-xml"); addEndpointToGroup("Security", "remove-password");
addEndpointToGroup("Convert", "html-to-pdf"); addEndpointToGroup("Security", "change-permissions");
addEndpointToGroup("Convert", "url-to-pdf"); addEndpointToGroup("Security", "add-watermark");
addEndpointToGroup("Convert", "markdown-to-pdf"); addEndpointToGroup("Security", "cert-sign");
addEndpointToGroup("Convert", "pdf-to-csv"); addEndpointToGroup("Security", "sanitize-pdf");
addEndpointToGroup("Security", "auto-redact");
// Adding endpoints to "Security" group
addEndpointToGroup("Security", "add-password"); // Adding endpoints to "Other" group
addEndpointToGroup("Security", "remove-password"); addEndpointToGroup("Other", "ocr-pdf");
addEndpointToGroup("Security", "change-permissions"); addEndpointToGroup("Other", "add-image");
addEndpointToGroup("Security", "add-watermark"); addEndpointToGroup("Other", "compress-pdf");
addEndpointToGroup("Security", "cert-sign"); addEndpointToGroup("Other", "extract-images");
addEndpointToGroup("Security", "sanitize-pdf"); addEndpointToGroup("Other", "change-metadata");
addEndpointToGroup("Security", "auto-redact"); addEndpointToGroup("Other", "extract-image-scans");
addEndpointToGroup("Other", "sign");
// Adding endpoints to "Other" group addEndpointToGroup("Other", "flatten");
addEndpointToGroup("Other", "ocr-pdf"); addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", "add-image"); addEndpointToGroup("Other", "remove-blanks");
addEndpointToGroup("Other", "compress-pdf"); addEndpointToGroup("Other", "remove-annotations");
addEndpointToGroup("Other", "extract-images"); addEndpointToGroup("Other", "compare");
addEndpointToGroup("Other", "change-metadata"); addEndpointToGroup("Other", "add-page-numbers");
addEndpointToGroup("Other", "extract-image-scans"); addEndpointToGroup("Other", "auto-rename");
addEndpointToGroup("Other", "sign"); addEndpointToGroup("Other", "get-info-on-pdf");
addEndpointToGroup("Other", "flatten"); addEndpointToGroup("Other", "show-javascript");
addEndpointToGroup("Other", "repair");
addEndpointToGroup("Other", REMOVE_BLANKS); // CLI
addEndpointToGroup("Other", "remove-annotations"); addEndpointToGroup("CLI", "compress-pdf");
addEndpointToGroup("Other", "compare"); addEndpointToGroup("CLI", "extract-image-scans");
addEndpointToGroup("Other", "add-page-numbers"); addEndpointToGroup("CLI", "remove-blanks");
addEndpointToGroup("Other", "auto-rename"); addEndpointToGroup("CLI", "repair");
addEndpointToGroup("Other", "get-info-on-pdf"); addEndpointToGroup("CLI", "pdf-to-pdfa");
addEndpointToGroup("Other", "show-javascript"); addEndpointToGroup("CLI", "file-to-pdf");
addEndpointToGroup("CLI", "xlsx-to-pdf");
// CLI addEndpointToGroup("CLI", "pdf-to-word");
addEndpointToGroup("CLI", "compress-pdf"); addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "extract-image-scans"); addEndpointToGroup("CLI", "pdf-to-text");
addEndpointToGroup("CLI", "repair"); addEndpointToGroup("CLI", "pdf-to-html");
addEndpointToGroup("CLI", "pdf-to-pdfa"); addEndpointToGroup("CLI", "pdf-to-xml");
addEndpointToGroup("CLI", "file-to-pdf"); addEndpointToGroup("CLI", "ocr-pdf");
addEndpointToGroup("CLI", "xlsx-to-pdf"); addEndpointToGroup("CLI", "html-to-pdf");
addEndpointToGroup("CLI", "pdf-to-word"); addEndpointToGroup("CLI", "url-to-pdf");
addEndpointToGroup("CLI", "pdf-to-presentation");
addEndpointToGroup("CLI", "pdf-to-html"); // python
addEndpointToGroup("CLI", "pdf-to-xml"); addEndpointToGroup("Python", "extract-image-scans");
addEndpointToGroup("CLI", "ocr-pdf"); addEndpointToGroup("Python", "remove-blanks");
addEndpointToGroup("CLI", "html-to-pdf"); addEndpointToGroup("Python", "html-to-pdf");
addEndpointToGroup("CLI", "url-to-pdf"); addEndpointToGroup("Python", "url-to-pdf");
addEndpointToGroup("CLI", "book-to-pdf");
addEndpointToGroup("CLI", "pdf-to-book"); // openCV
addEndpointToGroup("CLI", "pdf-to-rtf"); addEndpointToGroup("OpenCV", "extract-image-scans");
addEndpointToGroup("OpenCV", "remove-blanks");
// Calibre
addEndpointToGroup("Calibre", "book-to-pdf"); // LibreOffice
addEndpointToGroup("Calibre", "pdf-to-book"); addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf");
// python addEndpointToGroup("LibreOffice", "xlsx-to-pdf");
addEndpointToGroup("Python", "extract-image-scans"); addEndpointToGroup("LibreOffice", "pdf-to-word");
addEndpointToGroup("Python", REMOVE_BLANKS); addEndpointToGroup("LibreOffice", "pdf-to-presentation");
addEndpointToGroup("Python", "html-to-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-text");
addEndpointToGroup("Python", "url-to-pdf"); addEndpointToGroup("LibreOffice", "pdf-to-html");
addEndpointToGroup("LibreOffice", "pdf-to-xml");
// openCV
addEndpointToGroup("OpenCV", "extract-image-scans"); // OCRmyPDF
addEndpointToGroup("OpenCV", REMOVE_BLANKS); addEndpointToGroup("OCRmyPDF", "compress-pdf");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa");
// LibreOffice addEndpointToGroup("OCRmyPDF", "ocr-pdf");
addEndpointToGroup("LibreOffice", "repair");
addEndpointToGroup("LibreOffice", "file-to-pdf"); // Java
addEndpointToGroup("LibreOffice", "xlsx-to-pdf"); addEndpointToGroup("Java", "merge-pdfs");
addEndpointToGroup("LibreOffice", "pdf-to-word"); addEndpointToGroup("Java", "remove-pages");
addEndpointToGroup("LibreOffice", "pdf-to-presentation"); addEndpointToGroup("Java", "split-pdfs");
addEndpointToGroup("LibreOffice", "pdf-to-rtf"); addEndpointToGroup("Java", "pdf-organizer");
addEndpointToGroup("LibreOffice", "pdf-to-html"); addEndpointToGroup("Java", "rotate-pdf");
addEndpointToGroup("LibreOffice", "pdf-to-xml"); addEndpointToGroup("Java", "pdf-to-img");
addEndpointToGroup("Java", "img-to-pdf");
// OCRmyPDF addEndpointToGroup("Java", "add-password");
addEndpointToGroup("OCRmyPDF", "compress-pdf"); addEndpointToGroup("Java", "remove-password");
addEndpointToGroup("OCRmyPDF", "pdf-to-pdfa"); addEndpointToGroup("Java", "change-permissions");
addEndpointToGroup("OCRmyPDF", "ocr-pdf"); addEndpointToGroup("Java", "add-watermark");
addEndpointToGroup("Java", "add-image");
// Java addEndpointToGroup("Java", "extract-images");
addEndpointToGroup("Java", "merge-pdfs"); addEndpointToGroup("Java", "change-metadata");
addEndpointToGroup("Java", "remove-pages"); addEndpointToGroup("Java", "cert-sign");
addEndpointToGroup("Java", "split-pdfs"); addEndpointToGroup("Java", "multi-page-layout");
addEndpointToGroup("Java", "pdf-organizer"); addEndpointToGroup("Java", "scale-pages");
addEndpointToGroup("Java", "rotate-pdf"); addEndpointToGroup("Java", "add-page-numbers");
addEndpointToGroup("Java", "pdf-to-img"); addEndpointToGroup("Java", "auto-rename");
addEndpointToGroup("Java", "img-to-pdf"); addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Java", "add-password"); addEndpointToGroup("Java", "sanitize-pdf");
addEndpointToGroup("Java", "remove-password"); addEndpointToGroup("Java", "crop");
addEndpointToGroup("Java", "change-permissions"); addEndpointToGroup("Java", "get-info-on-pdf");
addEndpointToGroup("Java", "add-watermark"); addEndpointToGroup("Java", "extract-page");
addEndpointToGroup("Java", "add-image"); addEndpointToGroup("Java", "pdf-to-single-page");
addEndpointToGroup("Java", "extract-images"); addEndpointToGroup("Java", "markdown-to-pdf");
addEndpointToGroup("Java", "change-metadata"); addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "cert-sign"); addEndpointToGroup("Java", "auto-redact");
addEndpointToGroup("Java", "multi-page-layout"); addEndpointToGroup("Java", "pdf-to-csv");
addEndpointToGroup("Java", "scale-pages"); addEndpointToGroup("Java", "split-by-size-or-count");
addEndpointToGroup("Java", "add-page-numbers"); addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "auto-rename"); addEndpointToGroup("Java", "split-pdf-by-sections");
addEndpointToGroup("Java", "auto-split-pdf");
addEndpointToGroup("Java", "sanitize-pdf"); // Javascript
addEndpointToGroup("Java", "crop"); addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Java", "get-info-on-pdf"); addEndpointToGroup("Javascript", "sign");
addEndpointToGroup("Java", "extract-page"); addEndpointToGroup("Javascript", "compare");
addEndpointToGroup("Java", "pdf-to-single-page"); addEndpointToGroup("Javascript", "adjust-contrast");
addEndpointToGroup("Java", "markdown-to-pdf"); }
addEndpointToGroup("Java", "show-javascript");
addEndpointToGroup("Java", "auto-redact"); private void processEnvironmentConfigs() {
addEndpointToGroup("Java", "pdf-to-csv"); List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove();
addEndpointToGroup("Java", "split-by-size-or-count"); List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
addEndpointToGroup("Java", "overlay-pdf");
addEndpointToGroup("Java", "split-pdf-by-sections"); if (endpointsToRemove != null) {
addEndpointToGroup("Java", REMOVE_BLANKS); for (String endpoint : endpointsToRemove) {
addEndpointToGroup("Java", "pdf-to-text"); disableEndpoint(endpoint.trim());
}
// Javascript }
addEndpointToGroup("Javascript", "pdf-organizer");
addEndpointToGroup("Javascript", "sign"); if (groupsToRemove != null) {
addEndpointToGroup("Javascript", "compare"); for (String group : groupsToRemove) {
addEndpointToGroup("Javascript", "adjust-contrast"); disableGroup(group.trim());
} }
}
private void processEnvironmentConfigs() { }
List<String> endpointsToRemove = applicationProperties.getEndpoints().getToRemove(); }
List<String> groupsToRemove = applicationProperties.getEndpoints().getGroupsToRemove();
if (!bookAndHtmlFormatsInstalled) {
groupsToRemove.add("Calibre");
}
if (endpointsToRemove != null) {
for (String endpoint : endpointsToRemove) {
disableEndpoint(endpoint.trim());
}
}
if (groupsToRemove != null) {
for (String group : groupsToRemove) {
disableGroup(group.trim());
}
}
}
private static final String REMOVE_BLANKS = "remove-blanks";
}

View File

@@ -1,26 +1,26 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerInterceptor;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class EndpointInterceptor implements HandlerInterceptor { public class EndpointInterceptor implements HandlerInterceptor {
@Autowired private EndpointConfiguration endpointConfiguration; @Autowired private EndpointConfiguration endpointConfiguration;
@Override @Override
public boolean preHandle( public boolean preHandle(
HttpServletRequest request, HttpServletResponse response, Object handler) HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception { throws Exception {
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if (!endpointConfiguration.isEndpointEnabled(requestURI)) { if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
return false; return false;
} }
return true; return true;
} }
} }

View File

@@ -1,48 +0,0 @@
package stirling.software.SPDF.config;
import java.io.IOException;
import java.util.Map;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.templateresolver.AbstractConfigurableTemplateResolver;
import org.thymeleaf.templateresource.ClassLoaderTemplateResource;
import org.thymeleaf.templateresource.FileTemplateResource;
import org.thymeleaf.templateresource.ITemplateResource;
public class FileFallbackTemplateResolver extends AbstractConfigurableTemplateResolver {
private final ResourceLoader resourceLoader;
public FileFallbackTemplateResolver(ResourceLoader resourceLoader) {
super();
this.resourceLoader = resourceLoader;
setSuffix(".html");
}
// Note this does not work in local IDE, Prod jar only.
@Override
protected ITemplateResource computeTemplateResource(
IEngineConfiguration configuration,
String ownerTemplate,
String template,
String resourceName,
String characterEncoding,
Map<String, Object> templateResolutionAttributes) {
Resource resource =
resourceLoader.getResource("file:./customFiles/templates/" + resourceName);
try {
if (resource.exists() && resource.isReadable()) {
return new FileTemplateResource(resource.getFile().getPath(), characterEncoding);
}
} catch (IOException e) {
}
return new ClassLoaderTemplateResource(
Thread.currentThread().getContextClassLoader(),
"classpath:/templates/" + resourceName,
characterEncoding);
}
}

View File

@@ -1,25 +1,25 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.config.MeterFilter; import io.micrometer.core.instrument.config.MeterFilter;
import io.micrometer.core.instrument.config.MeterFilterReply; import io.micrometer.core.instrument.config.MeterFilterReply;
@Configuration @Configuration
public class MetricsConfig { public class MetricsConfig {
@Bean @Bean
public MeterFilter meterFilter() { public MeterFilter meterFilter() {
return new MeterFilter() { return new MeterFilter() {
@Override @Override
public MeterFilterReply accept(Meter.Id id) { public MeterFilterReply accept(Meter.Id id) {
if (id.getName().equals("http.requests")) { if (id.getName().equals("http.requests")) {
return MeterFilterReply.NEUTRAL; return MeterFilterReply.NEUTRAL;
} }
return MeterFilterReply.DENY; return MeterFilterReply.DENY;
} }
}; };
} }
} }

View File

@@ -1,64 +1,63 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.MeterRegistry;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@Component @Component
public class MetricsFilter extends OncePerRequestFilter { public class MetricsFilter extends OncePerRequestFilter {
private final MeterRegistry meterRegistry; private final MeterRegistry meterRegistry;
@Autowired @Autowired
public MetricsFilter(MeterRegistry meterRegistry) { public MetricsFilter(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry; this.meterRegistry = meterRegistry;
} }
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
// System.out.println("uri="+uri + ", method=" + request.getMethod() ); // System.out.println("uri="+uri + ", method=" + request.getMethod() );
// Ignore static resources // Ignore static resources
if (!(uri.startsWith("/js") if (!(uri.startsWith("/js")
|| uri.startsWith("/v1/api-docs") || uri.startsWith("/v1/api-docs")
|| uri.endsWith("robots.txt") || uri.endsWith("robots.txt")
|| uri.startsWith("/images") || uri.startsWith("/images")
|| uri.startsWith("/images") || uri.startsWith("/images")
|| uri.endsWith(".png") || uri.endsWith(".png")
|| uri.endsWith(".ico") || uri.endsWith(".ico")
|| uri.endsWith(".css") || uri.endsWith(".css")
|| uri.endsWith(".map") || uri.endsWith(".map")
|| uri.endsWith(".svg") || uri.endsWith(".svg")
|| uri.endsWith(".js") || uri.endsWith(".js")
|| uri.contains("swagger") || uri.contains("swagger")
|| uri.startsWith("/api/v1/info") || uri.startsWith("/api/v1/info")
|| uri.startsWith("/site.webmanifest") || uri.startsWith("/site.webmanifest")
|| uri.startsWith("/fonts") || uri.startsWith("/fonts")
|| uri.startsWith("/pdfjs"))) { || uri.startsWith("/pdfjs"))) {
Counter counter = Counter counter =
Counter.builder("http.requests") Counter.builder("http.requests")
.tag("uri", uri) .tag("uri", uri)
.tag("method", request.getMethod()) .tag("method", request.getMethod())
.register(meterRegistry); .register(meterRegistry);
counter.increment(); counter.increment();
// System.out.println("Counted"); }
}
filterChain.doFilter(request, response);
filterChain.doFilter(request, response); }
} }
}

View File

@@ -1,53 +1,53 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import io.swagger.v3.oas.models.Components; import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import stirling.software.SPDF.model.ApplicationProperties; import stirling.software.SPDF.model.ApplicationProperties;
@Configuration @Configuration
public class OpenApiConfig { public class OpenApiConfig {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
@Bean @Bean
public OpenAPI customOpenAPI() { public OpenAPI customOpenAPI() {
String version = getClass().getPackage().getImplementationVersion(); String version = getClass().getPackage().getImplementationVersion();
if (version == null) { if (version == null) {
version = "1.0.0"; // default version if all else fails version = "1.0.0"; // default version if all else fails
} }
SecurityScheme apiKeyScheme = SecurityScheme apiKeyScheme =
new SecurityScheme() new SecurityScheme()
.type(SecurityScheme.Type.APIKEY) .type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.name("X-API-KEY"); .name("X-API-KEY");
if (!applicationProperties.getSecurity().getEnableLogin()) { if (!applicationProperties.getSecurity().getEnableLogin()) {
return new OpenAPI() return new OpenAPI()
.components(new Components()) .components(new Components())
.info( .info(
new Info() new Info()
.title("Stirling PDF API") .title("Stirling PDF API")
.version(version) .version(version)
.description( .description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."));
} else { } else {
return new OpenAPI() return new OpenAPI()
.components(new Components().addSecuritySchemes("apiKey", apiKeyScheme)) .components(new Components().addSecuritySchemes("apiKey", apiKeyScheme))
.info( .info(
new Info() new Info()
.title("Stirling PDF API") .title("Stirling PDF API")
.version(version) .version(version)
.description( .description(
"API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")) "API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here."))
.addSecurityItem(new SecurityRequirement().addList("apiKey")); .addSecurityItem(new SecurityRequirement().addList("apiKey"));
} }
} }
} }

View File

@@ -1,7 +0,0 @@
package stirling.software.SPDF.config;
public interface ShowAdminInterface {
default boolean getShowUpdateOnlyAdmins() {
return true;
}
}

View File

@@ -1,18 +1,18 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> { public class StartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
public static LocalDateTime startTime; public static LocalDateTime startTime;
@Override @Override
public void onApplicationEvent(ContextRefreshedEvent event) { public void onApplicationEvent(ContextRefreshedEvent event) {
startTime = LocalDateTime.now(); startTime = LocalDateTime.now();
} }
} }

View File

@@ -1,26 +1,26 @@
package stirling.software.SPDF.config; package stirling.software.SPDF.config;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class WebMvcConfig implements WebMvcConfigurer { public class WebMvcConfig implements WebMvcConfigurer {
@Autowired private EndpointInterceptor endpointInterceptor; @Autowired private EndpointInterceptor endpointInterceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(endpointInterceptor); registry.addInterceptor(endpointInterceptor);
} }
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Handler for external static resources // Handler for external static resources
registry.addResourceHandler("/**") registry.addResourceHandler("/**")
.addResourceLocations("file:customFiles/static/", "classpath:/static/"); .addResourceLocations("file:customFiles/static/", "classpath:/static/");
// .setCachePeriod(0); // Optional: disable caching // .setCachePeriod(0); // Optional: disable caching
} }
} }

View File

@@ -1,46 +0,0 @@
package stirling.software.SPDF.config.security;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import stirling.software.SPDF.config.ShowAdminInterface;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository;
@Service
class AppUpdateAuthService implements ShowAdminInterface {
@Autowired private UserRepository userRepository;
@Autowired private ApplicationProperties applicationProperties;
public boolean getShowUpdateOnlyAdmins() {
boolean showUpdate = applicationProperties.getSystem().getShowUpdate();
if (!showUpdate) {
return showUpdate;
}
boolean showUpdateOnlyAdmin = applicationProperties.getSystem().getShowUpdateOnlyAdmin();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return !showUpdateOnlyAdmin;
}
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
return !showUpdateOnlyAdmin;
}
Optional<User> user = userRepository.findByUsername(authentication.getName());
if (user.isPresent() && showUpdateOnlyAdmin) {
return "ROLE_ADMIN".equals(user.get().getRolesAsString());
}
return showUpdate;
}
}

View File

@@ -1,79 +1,49 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.slf4j.Logger; import org.springframework.security.authentication.BadCredentialsException;
import org.slf4j.LoggerFactory; import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.authentication.InternalAuthenticationServiceException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.authentication.LockedException; import org.springframework.stereotype.Component;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import jakarta.servlet.ServletException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; @Component
import jakarta.servlet.http.HttpServletResponse; public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
import stirling.software.SPDF.model.User;
@Autowired private final LoginAttemptService loginAttemptService;
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private LoginAttemptService loginAttemptService; public CustomAuthenticationFailureHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
private UserService userService; }
private static final Logger logger = @Override
LoggerFactory.getLogger(CustomAuthenticationFailureHandler.class); public void onAuthenticationFailure(
HttpServletRequest request,
public CustomAuthenticationFailureHandler( HttpServletResponse response,
final LoginAttemptService loginAttemptService, UserService userService) { AuthenticationException exception)
this.loginAttemptService = loginAttemptService; throws IOException, ServletException {
this.userService = userService; String ip = request.getRemoteAddr();
} logger.error("Failed login attempt from IP: " + ip);
@Override String username = request.getParameter("username");
public void onAuthenticationFailure( if (loginAttemptService.loginAttemptCheck(username)) {
HttpServletRequest request, setDefaultFailureUrl("/login?error=locked");
HttpServletResponse response,
AuthenticationException exception) } else {
throws IOException, ServletException { if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
setDefaultFailureUrl("/login?error=badcredentials");
String ip = request.getRemoteAddr(); } else if (exception.getClass().isAssignableFrom(LockedException.class)) {
logger.error("Failed login attempt from IP: {}", ip); setDefaultFailureUrl("/login?error=locked");
}
if (exception.getClass().isAssignableFrom(InternalAuthenticationServiceException.class) }
|| "Password must not be null".equalsIgnoreCase(exception.getMessage())) {
response.sendRedirect("/login?error=oauth2AuthenticationError"); super.onAuthenticationFailure(request, response, exception);
return; }
} }
String username = request.getParameter("username");
if (username != null && !isDemoUser(username)) {
logger.info(
"Remaining attempts for user {}: {}",
username,
loginAttemptService.getRemainingAttempts(username));
loginAttemptService.loginFailed(username);
if (loginAttemptService.isBlocked(username)
|| exception.getClass().isAssignableFrom(LockedException.class)) {
response.sendRedirect("/login?error=locked");
return;
}
}
if (exception.getClass().isAssignableFrom(BadCredentialsException.class)
|| exception.getClass().isAssignableFrom(UsernameNotFoundException.class)) {
response.sendRedirect("/login?error=badcredentials");
return;
}
super.onAuthenticationFailure(request, response, exception);
}
private boolean isDemoUser(String username) {
Optional<User> user = userService.findByUsernameIgnoreCase(username);
return user.isPresent()
&& user.get().getAuthorities().stream()
.anyMatch(authority -> "ROLE_DEMO_USER".equals(authority.getAuthority()));
}
}

View File

@@ -2,9 +2,11 @@ package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest; import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
@@ -12,30 +14,25 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.utils.RequestUriUtils; import stirling.software.SPDF.utils.RequestUriUtils;
@Component
public class CustomAuthenticationSuccessHandler public class CustomAuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler { extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService; @Autowired private LoginAttemptService loginAttemptService;
public CustomAuthenticationSuccessHandler(LoginAttemptService loginAttemptService) {
this.loginAttemptService = loginAttemptService;
}
@Override @Override
public void onAuthenticationSuccess( public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication) HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException { throws ServletException, IOException {
String username = request.getParameter("username");
String userName = request.getParameter("username"); loginAttemptService.loginSucceeded(username);
loginAttemptService.loginSucceeded(userName);
// Get the saved request // Get the saved request
HttpSession session = request.getSession(false); HttpSession session = request.getSession(false);
SavedRequest savedRequest = SavedRequest savedRequest =
(session != null) session != null
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST") ? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null; : null;
if (savedRequest != null if (savedRequest != null
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) { && !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination // Redirect to the original destination

View File

@@ -1,33 +0,0 @@
package stirling.software.SPDF.config.security;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
@Autowired SessionRegistry sessionRegistry;
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId);
session.invalidate();
logger.debug("Session invalidated: " + sessionId);
}
response.sendRedirect(request.getContextPath() + "/login?logout=true");
}
}

View File

@@ -1,61 +1,57 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.model.User; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.UserRepository; import stirling.software.SPDF.repository.UserRepository;
@Service @Service
public class CustomUserDetailsService implements UserDetailsService { public class CustomUserDetailsService implements UserDetailsService {
@Autowired private UserRepository userRepository; @Autowired private UserRepository userRepository;
@Autowired private LoginAttemptService loginAttemptService; @Autowired private LoginAttemptService loginAttemptService;
@Override @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = User user =
userRepository userRepository
.findByUsername(username) .findByUsername(username)
.orElseThrow( .orElseThrow(
() -> () ->
new UsernameNotFoundException( new UsernameNotFoundException(
"No user found with username: " + username)); "No user found with username: " + username));
if (loginAttemptService.isBlocked(username)) { if (loginAttemptService.isBlocked(username)) {
throw new LockedException( throw new LockedException(
"Your account has been locked due to too many failed login attempts."); "Your account has been locked due to too many failed login attempts.");
} }
if (!user.hasPassword()) { return new org.springframework.security.core.userdetails.User(
throw new IllegalArgumentException("Password must not be null"); user.getUsername(),
} user.getPassword(),
user.isEnabled(),
return new org.springframework.security.core.userdetails.User( true,
user.getUsername(), true,
user.getPassword(), true,
user.isEnabled(), getAuthorities(user.getAuthorities()));
true, }
true,
true, private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) {
getAuthorities(user.getAuthorities())); return authorities.stream()
} .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
private Collection<? extends GrantedAuthority> getAuthorities(Set<Authority> authorities) { }
return authorities.stream() }
.map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
.collect(Collectors.toList());
}
}

View File

@@ -39,7 +39,7 @@ public class FirstLoginFilter extends OncePerRequestFilter {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) { if (authentication != null && authentication.isAuthenticated()) {
Optional<User> user = userService.findByUsernameIgnoreCase(authentication.getName()); Optional<User> user = userService.findByUsername(authentication.getName());
if ("GET".equalsIgnoreCase(method) if ("GET".equalsIgnoreCase(method)
&& user.isPresent() && user.isPresent()
&& user.get().isFirstLogin() && user.get().isFirstLogin()

View File

@@ -7,8 +7,6 @@ import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -21,67 +19,40 @@ public class InitialSecuritySetup {
@Autowired private UserService userService; @Autowired private UserService userService;
@Autowired private ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(InitialSecuritySetup.class);
@PostConstruct @PostConstruct
public void init() { public void init() {
if (!userService.hasUsers()) { if (!userService.hasUsers()) {
initializeAdminUser();
}
initializeInternalApiUser();
}
@PostConstruct String initialUsername =
public void initSecretKey() throws IOException { applicationProperties.getSecurity().getInitialLogin().getUsername();
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey(); String initialPassword =
if (!isValidUUID(secretKey)) { applicationProperties.getSecurity().getInitialLogin().getPassword();
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key if (initialUsername != null && initialPassword != null) {
saveKeyToConfig(secretKey);
}
}
private void initializeAdminUser() {
String initialUsername =
applicationProperties.getSecurity().getInitialLogin().getUsername();
String initialPassword =
applicationProperties.getSecurity().getInitialLogin().getPassword();
if (initialUsername != null
&& !initialUsername.isEmpty()
&& initialPassword != null
&& !initialPassword.isEmpty()
&& !userService.findByUsernameIgnoreCase(initialUsername).isPresent()) {
try {
userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId()); userService.saveUser(initialUsername, initialPassword, Role.ADMIN.getRoleId());
logger.info("Admin user created: " + initialUsername); } else {
} catch (IllegalArgumentException e) { initialUsername = "admin";
logger.error("Failed to initialize security setup", e); initialPassword = "stirling";
System.exit(1); userService.saveUser(
initialUsername, initialPassword, Role.ADMIN.getRoleId(), true);
} }
} else {
createDefaultAdminUser();
} }
} if (!userService.usernameExists(Role.INTERNAL_API_USER.getRoleId())) {
private void createDefaultAdminUser() {
String defaultUsername = "admin";
String defaultPassword = "stirling";
if (!userService.findByUsernameIgnoreCase(defaultUsername).isPresent()) {
userService.saveUser(defaultUsername, defaultPassword, Role.ADMIN.getRoleId(), true);
logger.info("Default admin user created: " + defaultUsername);
}
}
private void initializeInternalApiUser() {
if (!userService.usernameExistsIgnoreCase(Role.INTERNAL_API_USER.getRoleId())) {
userService.saveUser( userService.saveUser(
Role.INTERNAL_API_USER.getRoleId(), Role.INTERNAL_API_USER.getRoleId(),
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
Role.INTERNAL_API_USER.getRoleId()); Role.INTERNAL_API_USER.getRoleId());
userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId()); userService.addApiKeyToUser(Role.INTERNAL_API_USER.getRoleId());
logger.info("Internal API user created: " + Role.INTERNAL_API_USER.getRoleId()); }
}
@PostConstruct
public void initSecretKey() throws IOException {
String secretKey = applicationProperties.getAutomaticallyGenerated().getKey();
if (secretKey == null || secretKey.isEmpty()) {
secretKey = UUID.randomUUID().toString(); // Generating a random UUID as the secret key
saveKeyToConfig(secretKey);
} }
} }
@@ -114,16 +85,4 @@ public class InitialSecuritySetup {
// Write back to the file // Write back to the file
Files.write(path, lines); Files.write(path, lines);
} }
private boolean isValidUUID(String uuid) {
if (uuid == null) {
return false;
}
try {
UUID.fromString(uuid);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
} }

View File

@@ -3,8 +3,6 @@ package stirling.software.SPDF.config.security;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -17,62 +15,44 @@ public class LoginAttemptService {
@Autowired ApplicationProperties applicationProperties; @Autowired ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(LoginAttemptService.class); private int MAX_ATTEMPTS;
private int MAX_ATTEMPT;
private long ATTEMPT_INCREMENT_TIME; private long ATTEMPT_INCREMENT_TIME;
private ConcurrentHashMap<String, AttemptCounter> attemptsCache;
@PostConstruct @PostConstruct
public void init() { public void init() {
MAX_ATTEMPT = applicationProperties.getSecurity().getLoginAttemptCount(); MAX_ATTEMPTS = applicationProperties.getSecurity().getLoginAttemptCount();
ATTEMPT_INCREMENT_TIME = ATTEMPT_INCREMENT_TIME =
TimeUnit.MINUTES.toMillis( TimeUnit.MINUTES.toMillis(
applicationProperties.getSecurity().getLoginResetTimeMinutes()); applicationProperties.getSecurity().getLoginResetTimeMinutes());
attemptsCache = new ConcurrentHashMap<>();
} }
private final ConcurrentHashMap<String, AttemptCounter> attemptsCache =
new ConcurrentHashMap<>();
public void loginSucceeded(String key) { public void loginSucceeded(String key) {
logger.info(key + " " + attemptsCache.mappingCount()); attemptsCache.remove(key);
if (key == null || key.trim().isEmpty()) {
return;
}
attemptsCache.remove(key.toLowerCase());
} }
public void loginFailed(String key) { public boolean loginAttemptCheck(String key) {
if (key == null || key.trim().isEmpty()) return; attemptsCache.compute(
key,
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); (k, attemptCounter) -> {
if (attemptCounter == null) { if (attemptCounter == null
attemptCounter = new AttemptCounter(); || attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) {
attemptsCache.put(key.toLowerCase(), attemptCounter); return new AttemptCounter();
} else { } else {
if (attemptCounter.shouldReset(ATTEMPT_INCREMENT_TIME)) { attemptCounter.increment();
attemptCounter.reset(); return attemptCounter;
} }
attemptCounter.increment(); });
} return attemptsCache.get(key).getAttemptCount() >= MAX_ATTEMPTS;
} }
public boolean isBlocked(String key) { public boolean isBlocked(String key) {
if (key == null || key.trim().isEmpty()) return false; AttemptCounter attemptCounter = attemptsCache.get(key);
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase()); if (attemptCounter != null) {
if (attemptCounter == null) { return attemptCounter.getAttemptCount() >= MAX_ATTEMPTS;
return false;
} }
return false;
return attemptCounter.getAttemptCount() >= MAX_ATTEMPT;
}
public int getRemainingAttempts(String key) {
if (key == null || key.trim().isEmpty()) return MAX_ATTEMPT;
AttemptCounter attemptCounter = attemptsCache.get(key.toLowerCase());
if (attemptCounter == null) {
return MAX_ATTEMPT;
}
return MAX_ATTEMPT - attemptCounter.getAttemptCount();
} }
} }

View File

@@ -1,283 +1,137 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.*; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.context.annotation.Lazy; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import org.springframework.security.web.savedrequest.NullRequestCache;
import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import stirling.software.SPDF.repository.JPATokenRepositoryImpl;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.client.registration.ClientRegistration; @Configuration
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; @EnableWebSecurity()
import org.springframework.security.oauth2.client.registration.ClientRegistrations; @EnableMethodSecurity
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; public class SecurityConfiguration {
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.web.SecurityFilterChain; @Autowired private UserDetailsService userDetailsService;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; @Bean
import org.springframework.security.web.savedrequest.NullRequestCache; public PasswordEncoder passwordEncoder() {
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; return new BCryptPasswordEncoder();
}
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationFailureHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2AuthenticationSuccessHandler; @Autowired @Lazy private UserService userService;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2LogoutSuccessHandler;
import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService; @Autowired
import stirling.software.SPDF.model.ApplicationProperties; @Qualifier("loginEnabled")
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2; public boolean loginEnabledValue;
import stirling.software.SPDF.model.User;
import stirling.software.SPDF.repository.JPATokenRepositoryImpl; @Autowired private UserAuthenticationFilter userAuthenticationFilter;
@Configuration @Autowired private LoginAttemptService loginAttemptService;
@EnableWebSecurity()
@EnableMethodSecurity @Autowired private FirstLoginFilter firstLoginFilter;
public class SecurityConfiguration {
@Bean
@Autowired private CustomUserDetailsService userDetailsService; public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
@Bean
public PasswordEncoder passwordEncoder() { if (loginEnabledValue) {
return new BCryptPasswordEncoder();
} http.csrf(csrf -> csrf.disable());
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class);
@Autowired @Lazy private UserService userService; http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class);
http.formLogin(
@Autowired formLogin ->
@Qualifier("loginEnabled") formLogin
public boolean loginEnabledValue; .loginPage("/login")
.successHandler(
@Autowired ApplicationProperties applicationProperties; new CustomAuthenticationSuccessHandler())
.defaultSuccessUrl("/")
@Autowired private UserAuthenticationFilter userAuthenticationFilter; .failureHandler(
new CustomAuthenticationFailureHandler(
@Autowired private LoginAttemptService loginAttemptService; loginAttemptService))
.permitAll())
@Autowired private FirstLoginFilter firstLoginFilter; .requestCache(requestCache -> requestCache.requestCache(new NullRequestCache()))
.logout(
@Bean logout ->
public SessionRegistry sessionRegistry() { logout.logoutRequestMatcher(
return new SessionRegistryImpl(); new AntPathRequestMatcher("/logout"))
} .logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true) // Invalidate session
@Bean .deleteCookies("JSESSIONID", "remember-me"))
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .rememberMe(
http.addFilterBefore(userAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); rememberMeConfigurer ->
rememberMeConfigurer // Use the configurator directly
if (loginEnabledValue) { .key("uniqueAndSecret")
.tokenRepository(persistentTokenRepository())
http.csrf(csrf -> csrf.disable()); .tokenValiditySeconds(1209600) // 2 weeks
http.addFilterBefore(rateLimitingFilter(), UsernamePasswordAuthenticationFilter.class); )
http.addFilterAfter(firstLoginFilter, UsernamePasswordAuthenticationFilter.class); .authorizeHttpRequests(
http.sessionManagement( authz ->
sessionManagement -> authz.requestMatchers(
sessionManagement req -> {
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) String uri = req.getRequestURI();
.maximumSessions(10) String contextPath = req.getContextPath();
.maxSessionsPreventsLogin(false)
.sessionRegistry(sessionRegistry()) // Remove the context path from the URI
.expiredUrl("/login?logout=true")); String trimmedUri =
uri.startsWith(contextPath)
http.formLogin( ? uri.substring(
formLogin -> contextPath
formLogin .length())
.loginPage("/login") : uri;
.successHandler(
new CustomAuthenticationSuccessHandler( return trimmedUri.startsWith("/login")
loginAttemptService)) || trimmedUri.endsWith(".svg")
.defaultSuccessUrl("/") || trimmedUri.startsWith(
.failureHandler( "/register")
new CustomAuthenticationFailureHandler( || trimmedUri.startsWith("/error")
loginAttemptService, userService)) || trimmedUri.startsWith("/images/")
.permitAll()) || trimmedUri.startsWith("/public/")
.requestCache(requestCache -> requestCache.requestCache(new NullRequestCache())) || trimmedUri.startsWith("/css/")
.logout( || trimmedUri.startsWith("/js/");
logout -> })
logout.logoutRequestMatcher( .permitAll()
new AntPathRequestMatcher("/logout")) .anyRequest()
.logoutSuccessHandler(new CustomLogoutSuccessHandler()) .authenticated())
.invalidateHttpSession(true) // Invalidate session .userDetailsService(userDetailsService)
.deleteCookies("JSESSIONID", "remember-me")) .authenticationProvider(authenticationProvider());
.rememberMe( } else {
rememberMeConfigurer -> http.csrf(csrf -> csrf.disable())
rememberMeConfigurer // Use the configurator directly .authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
.key("uniqueAndSecret") }
.tokenRepository(persistentTokenRepository()) return http.build();
.tokenValiditySeconds(1209600) // 2 weeks }
)
.authorizeHttpRequests( @Bean
authz -> public IPRateLimitingFilter rateLimitingFilter() {
authz.requestMatchers( int maxRequestsPerIp = 1000000; // Example limit TODO add config level
req -> { return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
String uri = req.getRequestURI(); }
String contextPath = req.getContextPath();
@Bean
// Remove the context path from the URI public DaoAuthenticationProvider authenticationProvider() {
String trimmedUri = DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
uri.startsWith(contextPath) authProvider.setUserDetailsService(userDetailsService);
? uri.substring( authProvider.setPasswordEncoder(passwordEncoder());
contextPath return authProvider;
.length()) }
: uri;
@Bean
return trimmedUri.startsWith("/login") public PersistentTokenRepository persistentTokenRepository() {
|| trimmedUri.startsWith("/oauth") return new JPATokenRepositoryImpl();
|| trimmedUri.endsWith(".svg") }
|| trimmedUri.startsWith( }
"/register")
|| trimmedUri.startsWith("/error")
|| trimmedUri.startsWith("/images/")
|| trimmedUri.startsWith("/public/")
|| trimmedUri.startsWith("/css/")
|| trimmedUri.startsWith("/js/")
|| trimmedUri.startsWith(
"/api/v1/info/status");
})
.permitAll()
.anyRequest()
.authenticated())
.authenticationProvider(authenticationProvider());
// Handle OAUTH2 Logins
if (applicationProperties.getSecurity().getOAUTH2().getEnabled()) {
http.oauth2Login(
oauth2 ->
oauth2.loginPage("/oauth2")
/*
This Custom handler is used to check if the OAUTH2 user trying to log in, already exists in the database.
If user exists, login proceeds as usual. If user does not exist, then it is autocreated but only if 'OAUTH2AutoCreateUser'
is set as true, else login fails with an error message advising the same.
*/
.successHandler(
new CustomOAuth2AuthenticationSuccessHandler(
loginAttemptService,
applicationProperties,
userService))
.failureHandler(
new CustomOAuth2AuthenticationFailureHandler())
// Add existing Authorities from the database
.userInfoEndpoint(
userInfoEndpoint ->
userInfoEndpoint
.oidcUserService(
new CustomOAuth2UserService(
applicationProperties,
userService,
loginAttemptService))
.userAuthoritiesMapper(
userAuthoritiesMapper())))
.logout(
logout ->
logout.logoutSuccessHandler(
new CustomOAuth2LogoutSuccessHandler(
this.applicationProperties,
sessionRegistry())));
}
} else {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz.anyRequest().permitAll());
}
return http.build();
}
// Client Registration Repository for OAUTH2 OIDC Login
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
public ClientRegistrationRepository clientRegistrationRepository() {
return new InMemoryClientRegistrationRepository(this.oidcClientRegistration());
}
private ClientRegistration oidcClientRegistration() {
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
return ClientRegistrations.fromIssuerLocation(oauth.getIssuer())
.registrationId("oidc")
.clientId(oauth.getClientId())
.clientSecret(oauth.getClientSecret())
.scope(oauth.getScopes())
.userNameAttributeName(oauth.getUseAsUsername())
.clientName("OIDC")
.build();
}
/*
This following function is to grant Authorities to the OAUTH2 user from the values stored in the database.
This is required for the internal; 'hasRole()' function to give out the correct role.
*/
@Bean
@ConditionalOnProperty(
value = "security.oauth2.enabled",
havingValue = "true",
matchIfMissing = false)
GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(
authority -> {
// Add existing OAUTH2 Authorities
mappedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
// Add Authorities from database for existing user, if user is present.
if (authority instanceof OAuth2UserAuthority oauth2Auth) {
String useAsUsername =
applicationProperties
.getSecurity()
.getOAUTH2()
.getUseAsUsername();
Optional<User> userOpt =
userService.findByUsernameIgnoreCase(
(String) oauth2Auth.getAttributes().get(useAsUsername));
if (userOpt.isPresent()) {
User user = userOpt.get();
if (user != null) {
mappedAuthorities.add(
new SimpleGrantedAuthority(
userService.findRole(user).getAuthority()));
}
}
}
});
return mappedAuthorities;
};
}
@Bean
public IPRateLimitingFilter rateLimitingFilter() {
int maxRequestsPerIp = 1000000; // Example limit TODO add config level
return new IPRateLimitingFilter(maxRequestsPerIp, maxRequestsPerIp);
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
return new JPATokenRepositoryImpl();
}
@Bean
public boolean activSecurity() {
return true;
}
}

View File

@@ -1,118 +1,117 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import stirling.software.SPDF.model.ApiKeyAuthenticationToken; import stirling.software.SPDF.model.ApiKeyAuthenticationToken;
@Component @Component
public class UserAuthenticationFilter extends OncePerRequestFilter { public class UserAuthenticationFilter extends OncePerRequestFilter {
@Autowired private UserDetailsService userDetailsService; @Autowired private UserDetailsService userDetailsService;
@Autowired @Lazy private UserService userService; @Autowired @Lazy private UserService userService;
@Autowired @Autowired
@Qualifier("loginEnabled") @Qualifier("loginEnabled")
public boolean loginEnabledValue; public boolean loginEnabledValue;
@Override @Override
protected void doFilterInternal( protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
if (!loginEnabledValue) { if (!loginEnabledValue) {
// If login is not enabled, just pass all requests without authentication // If login is not enabled, just pass all requests without authentication
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// Check for API key in the request headers if no authentication exists // Check for API key in the request headers if no authentication exists
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String apiKey = request.getHeader("X-API-Key"); String apiKey = request.getHeader("X-API-Key");
if (apiKey != null && !apiKey.trim().isEmpty()) { if (apiKey != null && !apiKey.trim().isEmpty()) {
try { try {
// Use API key to authenticate. This requires you to have an authentication // Use API key to authenticate. This requires you to have an authentication
// provider for API keys. // provider for API keys.
UserDetails userDetails = userService.loadUserByApiKey(apiKey); UserDetails userDetails = userService.loadUserByApiKey(apiKey);
if (userDetails == null) { if (userDetails == null) {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
authentication = authentication =
new ApiKeyAuthenticationToken( new ApiKeyAuthenticationToken(
userDetails, apiKey, userDetails.getAuthorities()); userDetails, apiKey, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (AuthenticationException e) { } catch (AuthenticationException e) {
// If API key authentication fails, deny the request // If API key authentication fails, deny the request
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("Invalid API Key."); response.getWriter().write("Invalid API Key.");
return; return;
} }
} }
} }
// If we still don't have any authentication, deny the request // If we still don't have any authentication, deny the request
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
String method = request.getMethod(); String method = request.getMethod();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) { if ("GET".equalsIgnoreCase(method) && !(contextPath + "/login").equals(requestURI)) {
response.sendRedirect(contextPath + "/login"); // redirect to the login page response.sendRedirect(contextPath + "/login"); // redirect to the login page
return; return;
} else { } else {
response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter() response.getWriter()
.write( .write(
"Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternatively you can disable authentication if this is unexpected"); "Authentication required. Please provide a X-API-KEY in request header.\nThis is found in Settings -> Account Settings -> API Key\nAlternativly you can disable authentication if this is unexpected");
return; return;
} }
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
String contextPath = request.getContextPath(); String contextPath = request.getContextPath();
String[] permitAllPatterns = { String[] permitAllPatterns = {
contextPath + "/login", contextPath + "/login",
contextPath + "/register", contextPath + "/register",
contextPath + "/error", contextPath + "/error",
contextPath + "/images/", contextPath + "/images/",
contextPath + "/public/", contextPath + "/public/",
contextPath + "/css/", contextPath + "/css/",
contextPath + "/js/", contextPath + "/js/",
contextPath + "/pdfjs/", contextPath + "/pdfjs/",
contextPath + "/api/v1/info/status", contextPath + "/site.webmanifest"
contextPath + "/site.webmanifest" };
};
for (String pattern : permitAllPatterns) {
for (String pattern : permitAllPatterns) { if (uri.startsWith(pattern) || uri.endsWith(".svg")) {
if (uri.startsWith(pattern) || uri.endsWith(".svg")) { return true;
return true; }
} }
}
return false;
return false; }
} }
}

View File

@@ -1,148 +1,143 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe; import io.github.bucket4j.ConsumptionProbe;
import io.github.bucket4j.Refill; import io.github.bucket4j.Refill;
import io.github.pixee.security.Newlines;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException;
import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponse; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.Role;
@Component
@Component public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
public class UserBasedRateLimitingFilter extends OncePerRequestFilter {
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> apiBuckets = new ConcurrentHashMap<>(); private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
private final Map<String, Bucket> webBuckets = new ConcurrentHashMap<>();
@Autowired private UserDetailsService userDetailsService;
@Autowired private UserDetailsService userDetailsService;
@Autowired
@Autowired @Qualifier("rateLimit")
@Qualifier("rateLimit") public boolean rateLimit;
public boolean rateLimit;
@Override
@Override protected void doFilterInternal(
protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
throws ServletException, IOException { if (!rateLimit) {
if (!rateLimit) { // If rateLimit is not enabled, just pass all requests without rate limiting
// If rateLimit is not enabled, just pass all requests without rate limiting filterChain.doFilter(request, response);
filterChain.doFilter(request, response); return;
return; }
}
String method = request.getMethod();
String method = request.getMethod(); if (!"POST".equalsIgnoreCase(method)) {
if (!"POST".equalsIgnoreCase(method)) { // If the request is not a POST, just pass it through without rate limiting
// If the request is not a POST, just pass it through without rate limiting filterChain.doFilter(request, response);
filterChain.doFilter(request, response); return;
return; }
}
String identifier = null;
String identifier = null;
// Check for API key in the request headers
// Check for API key in the request headers String apiKey = request.getHeader("X-API-Key");
String apiKey = request.getHeader("X-API-Key"); if (apiKey != null && !apiKey.trim().isEmpty()) {
if (apiKey != null && !apiKey.trim().isEmpty()) { identifier =
identifier = "API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames
"API_KEY_" + apiKey; // Prefix to distinguish between API keys and usernames } else {
} else { Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) {
if (authentication != null && authentication.isAuthenticated()) { UserDetails userDetails = (UserDetails) authentication.getPrincipal();
UserDetails userDetails = (UserDetails) authentication.getPrincipal(); identifier = userDetails.getUsername();
identifier = userDetails.getUsername(); }
} }
}
// If neither API key nor an authenticated user is present, use IP address
// If neither API key nor an authenticated user is present, use IP address if (identifier == null) {
if (identifier == null) { identifier = request.getRemoteAddr();
identifier = request.getRemoteAddr(); }
}
Role userRole =
Role userRole = getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
getRoleFromAuthentication(SecurityContextHolder.getContext().getAuthentication());
if (request.getHeader("X-API-Key") != null) {
if (request.getHeader("X-API-Key") != null) { // It's an API call
// It's an API call processRequest(
processRequest( userRole.getApiCallsPerDay(),
userRole.getApiCallsPerDay(), identifier,
identifier, apiBuckets,
apiBuckets, request,
request, response,
response, filterChain);
filterChain); } else {
} else { // It's a Web UI call
// It's a Web UI call processRequest(
processRequest( userRole.getWebCallsPerDay(),
userRole.getWebCallsPerDay(), identifier,
identifier, webBuckets,
webBuckets, request,
request, response,
response, filterChain);
filterChain); }
} }
}
private Role getRoleFromAuthentication(Authentication authentication) {
private Role getRoleFromAuthentication(Authentication authentication) { if (authentication != null && authentication.isAuthenticated()) {
if (authentication != null && authentication.isAuthenticated()) { for (GrantedAuthority authority : authentication.getAuthorities()) {
for (GrantedAuthority authority : authentication.getAuthorities()) { try {
try { return Role.fromString(authority.getAuthority());
return Role.fromString(authority.getAuthority()); } catch (IllegalArgumentException ex) {
} catch (IllegalArgumentException ex) { // Ignore and continue to next authority.
// Ignore and continue to next authority. }
} }
} }
} throw new IllegalStateException("User does not have a valid role.");
throw new IllegalStateException("User does not have a valid role."); }
}
private void processRequest(
private void processRequest( int limitPerDay,
int limitPerDay, String identifier,
String identifier, Map<String, Bucket> buckets,
Map<String, Bucket> buckets, HttpServletRequest request,
HttpServletRequest request, HttpServletResponse response,
HttpServletResponse response, FilterChain filterChain)
FilterChain filterChain) throws IOException, ServletException {
throws IOException, ServletException { Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay));
Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket(limitPerDay)); ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
ConsumptionProbe probe = userBucket.tryConsumeAndReturnRemaining(1);
if (probe.isConsumed()) {
if (probe.isConsumed()) { response.setHeader("X-Rate-Limit-Remaining", Long.toString(probe.getRemainingTokens()));
response.setHeader( filterChain.doFilter(request, response);
"X-Rate-Limit-Remaining", } else {
Newlines.stripAll(Long.toString(probe.getRemainingTokens()))); long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000;
filterChain.doFilter(request, response); response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
} else { response.setHeader("X-Rate-Limit-Retry-After-Seconds", String.valueOf(waitForRefill));
long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; response.getWriter().write("Rate limit exceeded for POST requests.");
response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); }
response.setHeader( }
"X-Rate-Limit-Retry-After-Seconds",
Newlines.stripAll(String.valueOf(waitForRefill))); private Bucket createUserBucket(int limitPerDay) {
response.getWriter().write("Rate limit exceeded for POST requests."); Bandwidth limit =
} Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
} return Bucket.builder().addLimit(limit).build();
}
private Bucket createUserBucket(int limitPerDay) { }
Bandwidth limit =
Bandwidth.classic(limitPerDay, Refill.intervally(limitPerDay, Duration.ofDays(1)));
return Bucket.builder().addLimit(limit).build();
}
}

View File

@@ -1,293 +1,197 @@
package stirling.software.SPDF.config.security; package stirling.software.SPDF.config.security;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.security.core.Authentication;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface;
import stirling.software.SPDF.model.Authority;
import stirling.software.SPDF.controller.api.pipeline.UserServiceInterface; import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.AuthenticationType; import stirling.software.SPDF.model.User;
import stirling.software.SPDF.model.Authority; import stirling.software.SPDF.repository.UserRepository;
import stirling.software.SPDF.model.Role;
import stirling.software.SPDF.model.User; @Service
import stirling.software.SPDF.repository.AuthorityRepository; public class UserService implements UserServiceInterface {
import stirling.software.SPDF.repository.UserRepository;
@Autowired private UserRepository userRepository;
@Service
public class UserService implements UserServiceInterface { @Autowired private PasswordEncoder passwordEncoder;
@Autowired private UserRepository userRepository; public Authentication getAuthentication(String apiKey) {
User user = getUserByApiKey(apiKey);
@Autowired private AuthorityRepository authorityRepository; if (user == null) {
throw new UsernameNotFoundException("API key is not valid");
@Autowired private PasswordEncoder passwordEncoder; }
@Autowired private MessageSource messageSource; // Convert the user into an Authentication object
return new UsernamePasswordAuthenticationToken(
// Handle OAUTH2 login and user auto creation. user, // principal (typically the user)
public boolean processOAuth2PostLogin(String username, boolean autoCreateUser) { null, // credentials (we don't expose the password or API key here)
if (!isUsernameValid(username)) { getAuthorities(user) // user's authorities (roles/permissions)
return false; );
} }
Optional<User> existingUser = userRepository.findByUsernameIgnoreCase(username);
if (existingUser.isPresent()) { private Collection<? extends GrantedAuthority> getAuthorities(User user) {
return true; // Convert each Authority object into a SimpleGrantedAuthority object.
} return user.getAuthorities().stream()
if (autoCreateUser) { .map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority()))
saveUser(username, AuthenticationType.OAUTH2); .collect(Collectors.toList());
return true; }
}
return false; private String generateApiKey() {
} String apiKey;
do {
public Authentication getAuthentication(String apiKey) { apiKey = UUID.randomUUID().toString();
User user = getUserByApiKey(apiKey); } while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness
if (user == null) { return apiKey;
throw new UsernameNotFoundException("API key is not valid"); }
}
public User addApiKeyToUser(String username) {
// Convert the user into an Authentication object User user =
return new UsernamePasswordAuthenticationToken( userRepository
user, // principal (typically the user) .findByUsername(username)
null, // credentials (we don't expose the password or API key here) .orElseThrow(() -> new UsernameNotFoundException("User not found"));
getAuthorities(user) // user's authorities (roles/permissions)
); user.setApiKey(generateApiKey());
} return userRepository.save(user);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
// Convert each Authority object into a SimpleGrantedAuthority object. public User refreshApiKeyForUser(String username) {
return user.getAuthorities().stream() return addApiKeyToUser(username); // reuse the add API key method for refreshing
.map((Authority authority) -> new SimpleGrantedAuthority(authority.getAuthority())) }
.collect(Collectors.toList());
} public String getApiKeyForUser(String username) {
User user =
private String generateApiKey() { userRepository
String apiKey; .findByUsername(username)
do { .orElseThrow(() -> new UsernameNotFoundException("User not found"));
apiKey = UUID.randomUUID().toString(); return user.getApiKey();
} while (userRepository.findByApiKey(apiKey) != null); // Ensure uniqueness }
return apiKey;
} public boolean isValidApiKey(String apiKey) {
return userRepository.findByApiKey(apiKey) != null;
public User addApiKeyToUser(String username) { }
User user =
userRepository public User getUserByApiKey(String apiKey) {
.findByUsernameIgnoreCase(username) return userRepository.findByApiKey(apiKey);
.orElseThrow(() -> new UsernameNotFoundException("User not found")); }
user.setApiKey(generateApiKey()); public UserDetails loadUserByApiKey(String apiKey) {
return userRepository.save(user); User userOptional = userRepository.findByApiKey(apiKey);
} if (userOptional != null) {
User user = userOptional;
public User refreshApiKeyForUser(String username) { // Convert your User entity to a UserDetails object with authorities
return addApiKeyToUser(username); // reuse the add API key method for refreshing return new org.springframework.security.core.userdetails.User(
} user.getUsername(),
user.getPassword(), // you might not need this for API key auth
public String getApiKeyForUser(String username) { getAuthorities(user));
User user = }
userRepository return null; // or throw an exception
.findByUsernameIgnoreCase(username) }
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return user.getApiKey(); public boolean validateApiKeyForUser(String username, String apiKey) {
} Optional<User> userOpt = userRepository.findByUsername(username);
return userOpt.isPresent() && userOpt.get().getApiKey().equals(apiKey);
public boolean isValidApiKey(String apiKey) { }
return userRepository.findByApiKey(apiKey) != null;
} public void saveUser(String username, String password) {
User user = new User();
public User getUserByApiKey(String apiKey) { user.setUsername(username);
return userRepository.findByApiKey(apiKey); user.setPassword(passwordEncoder.encode(password));
} user.setEnabled(true);
userRepository.save(user);
public UserDetails loadUserByApiKey(String apiKey) { }
User user = userRepository.findByApiKey(apiKey);
if (user != null) { public void saveUser(String username, String password, String role, boolean firstLogin) {
// Convert your User entity to a UserDetails object with authorities User user = new User();
return new org.springframework.security.core.userdetails.User( user.setUsername(username);
user.getUsername(), user.setPassword(passwordEncoder.encode(password));
user.getPassword(), // you might not need this for API key auth user.addAuthority(new Authority(role, user));
getAuthorities(user)); user.setEnabled(true);
} user.setFirstLogin(firstLogin);
return null; // or throw an exception userRepository.save(user);
} }
public boolean validateApiKeyForUser(String username, String apiKey) { public void saveUser(String username, String password, String role) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); User user = new User();
return userOpt.isPresent() && apiKey.equals(userOpt.get().getApiKey()); user.setUsername(username);
} user.setPassword(passwordEncoder.encode(password));
user.addAuthority(new Authority(role, user));
public void saveUser(String username, AuthenticationType authenticationType) user.setEnabled(true);
throws IllegalArgumentException { user.setFirstLogin(false);
if (!isUsernameValid(username)) { userRepository.save(user);
throw new IllegalArgumentException(getInvalidUsernameMessage()); }
}
User user = new User(); public void deleteUser(String username) {
user.setUsername(username); Optional<User> userOpt = userRepository.findByUsername(username);
user.setEnabled(true); if (userOpt.isPresent()) {
user.setFirstLogin(false); for (Authority authority : userOpt.get().getAuthorities()) {
user.addAuthority(new Authority(Role.USER.getRoleId(), user)); if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
user.setAuthenticationType(authenticationType); return;
userRepository.save(user); }
} }
userRepository.delete(userOpt.get());
public void saveUser(String username, String password) throws IllegalArgumentException { }
if (!isUsernameValid(username)) { }
throw new IllegalArgumentException(getInvalidUsernameMessage());
} public boolean usernameExists(String username) {
User user = new User(); return userRepository.findByUsername(username).isPresent();
user.setUsername(username); }
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true); public boolean hasUsers() {
user.setAuthenticationType(AuthenticationType.WEB); return userRepository.count() > 0;
userRepository.save(user); }
}
public void updateUserSettings(String username, Map<String, String> updates) {
public void saveUser(String username, String password, String role, boolean firstLogin) Optional<User> userOpt = userRepository.findByUsername(username);
throws IllegalArgumentException { if (userOpt.isPresent()) {
if (!isUsernameValid(username)) { User user = userOpt.get();
throw new IllegalArgumentException(getInvalidUsernameMessage()); Map<String, String> settingsMap = user.getSettings();
}
User user = new User(); if (settingsMap == null) {
user.setUsername(username); settingsMap = new HashMap<String, String>();
user.setPassword(passwordEncoder.encode(password)); }
user.addAuthority(new Authority(role, user)); settingsMap.clear();
user.setEnabled(true); settingsMap.putAll(updates);
user.setAuthenticationType(AuthenticationType.WEB); user.setSettings(settingsMap);
user.setFirstLogin(firstLogin);
userRepository.save(user); userRepository.save(user);
} }
}
public void saveUser(String username, String password, String role)
throws IllegalArgumentException { public Optional<User> findByUsername(String username) {
saveUser(username, password, role, false); return userRepository.findByUsername(username);
} }
public void deleteUser(String username) { public void changeUsername(User user, String newUsername) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username); user.setUsername(newUsername);
if (userOpt.isPresent()) { userRepository.save(user);
for (Authority authority : userOpt.get().getAuthorities()) { }
if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) {
return; public void changePassword(User user, String newPassword) {
} user.setPassword(passwordEncoder.encode(newPassword));
} userRepository.save(user);
userRepository.delete(userOpt.get()); }
}
} public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
public boolean usernameExists(String username) { userRepository.save(user);
return userRepository.findByUsername(username).isPresent(); }
}
public boolean isPasswordCorrect(User user, String currentPassword) {
public boolean usernameExistsIgnoreCase(String username) { return passwordEncoder.matches(currentPassword, user.getPassword());
return userRepository.findByUsernameIgnoreCase(username).isPresent(); }
} }
public boolean hasUsers() {
long userCount = userRepository.count();
if (userRepository
.findByUsernameIgnoreCase(Role.INTERNAL_API_USER.getRoleId())
.isPresent()) {
userCount -= 1;
}
return userCount > 0;
}
public void updateUserSettings(String username, Map<String, String> updates) {
Optional<User> userOpt = userRepository.findByUsernameIgnoreCase(username);
if (userOpt.isPresent()) {
User user = userOpt.get();
Map<String, String> settingsMap = user.getSettings();
if (settingsMap == null) {
settingsMap = new HashMap<>();
}
settingsMap.clear();
settingsMap.putAll(updates);
user.setSettings(settingsMap);
userRepository.save(user);
}
}
public Optional<User> findByUsername(String username) {
return userRepository.findByUsername(username);
}
public Optional<User> findByUsernameIgnoreCase(String username) {
return userRepository.findByUsernameIgnoreCase(username);
}
public Authority findRole(User user) {
return authorityRepository.findByUserId(user.getId());
}
public void changeUsername(User user, String newUsername) throws IllegalArgumentException {
if (!isUsernameValid(newUsername)) {
throw new IllegalArgumentException(getInvalidUsernameMessage());
}
user.setUsername(newUsername);
userRepository.save(user);
}
public void changePassword(User user, String newPassword) {
user.setPassword(passwordEncoder.encode(newPassword));
userRepository.save(user);
}
public void changeFirstUse(User user, boolean firstUse) {
user.setFirstLogin(firstUse);
userRepository.save(user);
}
public void changeRole(User user, String newRole) {
Authority userAuthority = this.findRole(user);
userAuthority.setAuthority(newRole);
authorityRepository.save(userAuthority);
}
public boolean isPasswordCorrect(User user, String currentPassword) {
return passwordEncoder.matches(currentPassword, user.getPassword());
}
public boolean isUsernameValid(String username) {
// Checks whether the simple username is formatted correctly
boolean isValidSimpleUsername =
username.matches("^[a-zA-Z0-9][a-zA-Z0-9@._+-]*[a-zA-Z0-9]$");
// Checks whether the email address is formatted correctly
boolean isValidEmail =
username.matches(
"^(?=.{1,64}@)[A-Za-z0-9]+(\\.[A-Za-z0-9_+.-]+)*@[^-][A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)*(\\.[A-Za-z]{2,})$");
return isValidSimpleUsername || isValidEmail;
}
private String getInvalidUsernameMessage() {
return messageSource.getMessage(
"invalidUsernameMessage", null, LocaleContextHolder.getLocale());
}
public boolean hasPassword(String username) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
return user.isPresent() && user.get().hasPassword();
}
public boolean isAuthenticationTypeByUsername(
String username, AuthenticationType authenticationType) {
Optional<User> user = userRepository.findByUsernameIgnoreCase(username);
return user.isPresent()
&& authenticationType.name().equalsIgnoreCase(user.get().getAuthenticationType());
}
}

View File

@@ -1,49 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class CustomOAuth2AuthenticationFailureHandler
extends SimpleUrlAuthenticationFailureHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationFailureHandler.class);
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception)
throws IOException, ServletException {
if (exception instanceof OAuth2AuthenticationException) {
OAuth2Error error = ((OAuth2AuthenticationException) exception).getError();
String errorCode = error.getErrorCode();
if (error.getErrorCode().equals("Password must not be null")) {
errorCode = "userAlreadyExistsWeb";
}
logger.error("OAuth2 Authentication error: " + errorCode);
getRedirectStrategy()
.sendRedirect(request, response, "/logout?erroroauth=" + errorCode);
return;
} else if (exception instanceof LockedException) {
logger.error("Account locked: ", exception);
getRedirectStrategy().sendRedirect(request, response, "/logout?error=locked");
} else {
logger.error("Unhandled authentication exception", exception);
super.onAuthenticationFailure(request, response, exception);
}
}
}

View File

@@ -1,93 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.SavedRequest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
import stirling.software.SPDF.model.AuthenticationType;
import stirling.software.SPDF.utils.RequestUriUtils;
public class CustomOAuth2AuthenticationSuccessHandler
extends SavedRequestAwareAuthenticationSuccessHandler {
private LoginAttemptService loginAttemptService;
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2AuthenticationSuccessHandler.class);
private ApplicationProperties applicationProperties;
private UserService userService;
public CustomOAuth2AuthenticationSuccessHandler(
final LoginAttemptService loginAttemptService,
ApplicationProperties applicationProperties,
UserService userService) {
this.applicationProperties = applicationProperties;
this.userService = userService;
this.loginAttemptService = loginAttemptService;
}
@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
// Get the saved request
HttpSession session = request.getSession(false);
SavedRequest savedRequest =
(session != null)
? (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST")
: null;
if (savedRequest != null
&& !RequestUriUtils.isStaticResource(savedRequest.getRedirectUrl())) {
// Redirect to the original destination
super.onAuthenticationSuccess(request, response, authentication);
} else {
OAuth2User oauthUser = (OAuth2User) authentication.getPrincipal();
OAUTH2 oAuth = applicationProperties.getSecurity().getOAUTH2();
String username = oauthUser.getName();
if (loginAttemptService.isBlocked(username)) {
if (session != null) {
session.removeAttribute("SPRING_SECURITY_SAVED_REQUEST");
}
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.usernameExistsIgnoreCase(username)
&& userService.hasPassword(username)
&& !userService.isAuthenticationTypeByUsername(
username, AuthenticationType.OAUTH2)
&& oAuth.getAutoCreateUser()) {
response.sendRedirect(
request.getContextPath() + "/logout?oauth2AuthenticationErrorWeb=true");
return;
} else {
try {
userService.processOAuth2PostLogin(username, oAuth.getAutoCreateUser());
response.sendRedirect("/");
return;
} catch (IllegalArgumentException e) {
response.sendRedirect("/logout?invalidUsername=true");
return;
}
}
}
}
}

View File

@@ -1,86 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
public class CustomOAuth2LogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private static final Logger logger =
LoggerFactory.getLogger(CustomOAuth2LogoutSuccessHandler.class);
private final SessionRegistry sessionRegistry;
private final ApplicationProperties applicationProperties;
public CustomOAuth2LogoutSuccessHandler(
ApplicationProperties applicationProperties, SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
this.applicationProperties = applicationProperties;
}
@Override
public void onLogoutSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String param = "logout=true";
OAUTH2 oauth = applicationProperties.getSecurity().getOAUTH2();
String provider = oauth.getProvider() != null ? oauth.getProvider() : "";
if (request.getParameter("oauth2AuthenticationErrorWeb") != null) {
param = "erroroauth=oauth2AuthenticationErrorWeb";
} else if (request.getParameter("error") != null) {
param = "error=" + request.getParameter("error");
} else if (request.getParameter("erroroauth") != null) {
param = "erroroauth=" + request.getParameter("erroroauth");
} else if (request.getParameter("oauth2AutoCreateDisabled") != null) {
param = "error=oauth2AutoCreateDisabled";
}
HttpSession session = request.getSession(false);
if (session != null) {
String sessionId = session.getId();
sessionRegistry.removeSessionInformation(sessionId);
session.invalidate();
logger.debug("Session invalidated: " + sessionId);
}
switch (provider) {
case "keycloak":
String logoutUrl =
oauth.getIssuer()
+ "/protocol/openid-connect/logout"
+ "?client_id="
+ oauth.getClientId()
+ "&post_logout_redirect_uri="
+ response.encodeRedirectURL(
request.getScheme()
+ "://"
+ request.getHeader("host")
+ "/login?"
+ param);
logger.debug("Redirecting to Keycloak logout URL: " + logoutUrl);
response.sendRedirect(logoutUrl);
break;
case "google":
// Add Google specific logout URL if needed
default:
String redirectUrl = request.getContextPath() + "/login?" + param;
logger.debug("Redirecting to default logout URL: " + redirectUrl);
response.sendRedirect(redirectUrl);
break;
}
}
}

View File

@@ -1,73 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import stirling.software.SPDF.config.security.LoginAttemptService;
import stirling.software.SPDF.config.security.UserService;
import stirling.software.SPDF.model.ApplicationProperties;
import stirling.software.SPDF.model.User;
public class CustomOAuth2UserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private final OidcUserService delegate = new OidcUserService();
private UserService userService;
private LoginAttemptService loginAttemptService;
private ApplicationProperties applicationProperties;
private static final Logger logger = LoggerFactory.getLogger(CustomOAuth2UserService.class);
public CustomOAuth2UserService(
ApplicationProperties applicationProperties,
UserService userService,
LoginAttemptService loginAttemptService) {
this.applicationProperties = applicationProperties;
this.userService = userService;
this.loginAttemptService = loginAttemptService;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
String usernameAttribute =
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
try {
OidcUser user = delegate.loadUser(userRequest);
String username = user.getUserInfo().getClaimAsString(usernameAttribute);
Optional<User> duser = userService.findByUsernameIgnoreCase(username);
if (duser.isPresent()) {
if (loginAttemptService.isBlocked(username)) {
throw new LockedException(
"Your account has been locked due to too many failed login attempts.");
}
if (userService.hasPassword(username)) {
throw new IllegalArgumentException("Password must not be null");
}
}
// Return a new OidcUser with adjusted attributes
return new DefaultOidcUser(
user.getAuthorities(),
userRequest.getIdToken(),
user.getUserInfo(),
usernameAttribute);
} catch (java.lang.IllegalArgumentException e) {
logger.error("Error loading OIDC user: {}", e.getMessage());
throw new OAuth2AuthenticationException(new OAuth2Error(e.getMessage()), e);
} catch (Exception e) {
logger.error("Unexpected error loading OIDC user", e);
throw new OAuth2AuthenticationException("Unexpected error during authentication");
}
}
}

View File

@@ -1,57 +0,0 @@
package stirling.software.SPDF.config.security.oauth2;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import stirling.software.SPDF.model.ApplicationProperties;
public class CustomOAuthUserService implements OAuth2UserService<OidcUserRequest, OidcUser> {
private static final Logger logger = LoggerFactory.getLogger(CustomOAuthUserService.class);
private final OidcUserService delegate = new OidcUserService();
private ApplicationProperties applicationProperties;
public CustomOAuthUserService(ApplicationProperties applicationProperties) {
this.applicationProperties = applicationProperties;
}
@Override
public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
String usernameAttribute =
applicationProperties.getSecurity().getOAUTH2().getUseAsUsername();
try {
OidcUser user = delegate.loadUser(userRequest);
Map<String, Object> attributes = new HashMap<>(user.getAttributes());
// Ensure the preferred username attribute is present
if (!attributes.containsKey(usernameAttribute)) {
attributes.put(usernameAttribute, attributes.getOrDefault("email", ""));
usernameAttribute = "email";
logger.info("Adjusted username attribute to use email");
}
// Return a new OidcUser with adjusted attributes
return new DefaultOidcUser(
user.getAuthorities(),
userRequest.getIdToken(),
user.getUserInfo(),
usernameAttribute);
} catch (java.lang.IllegalArgumentException e) {
throw new OAuth2AuthenticationException(
new OAuth2Error(e.getMessage()), e.getMessage(), e);
}
}
}

View File

@@ -1,14 +1,13 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode;
import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -38,7 +37,9 @@ public class CropController {
description = description =
"This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO") "This operation takes an input PDF file and crops it according to the given coordinates. Input:PDF Output:PDF Type:SISO")
public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException { public ResponseEntity<byte[]> cropPdf(@ModelAttribute CropPdfForm form) throws IOException {
PDDocument sourceDocument = Loader.loadPDF(form.getFileInput().getBytes());
PDDocument sourceDocument =
PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()));
PDDocument newDocument = new PDDocument(); PDDocument newDocument = new PDDocument();
@@ -52,8 +53,7 @@ public class CropController {
// Create a new page with the size of the source page // Create a new page with the size of the source page
PDPage newPage = new PDPage(sourcePage.getMediaBox()); PDPage newPage = new PDPage(sourcePage.getMediaBox());
newDocument.addPage(newPage); newDocument.addPage(newPage);
PDPageContentStream contentStream = PDPageContentStream contentStream = new PDPageContentStream(newDocument, newPage);
new PDPageContentStream(newDocument, newPage, AppendMode.OVERWRITE, true, true);
// Import the source page as a form XObject // Import the source page as a form XObject
PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i); PDFormXObject formXObject = layerUtility.importPageAsForm(sourceDocument, i);

View File

@@ -1,17 +1,16 @@
package stirling.software.SPDF.controller.api; package stirling.software.SPDF.controller.api;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
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 org.apache.pdfbox.Loader; import org.apache.pdfbox.io.MemoryUsageSetting;
import org.apache.pdfbox.multipdf.PDFMergerUtility; import org.apache.pdfbox.multipdf.PDFMergerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
@@ -28,7 +27,6 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import stirling.software.SPDF.model.api.general.MergePdfsRequest; import stirling.software.SPDF.model.api.general.MergePdfsRequest;
import stirling.software.SPDF.utils.GeneralUtils;
import stirling.software.SPDF.utils.WebResponseUtils; import stirling.software.SPDF.utils.WebResponseUtils;
@RestController @RestController
@@ -38,7 +36,7 @@ public class MergeController {
private static final Logger logger = LoggerFactory.getLogger(MergeController.class); private static final Logger logger = LoggerFactory.getLogger(MergeController.class);
public PDDocument mergeDocuments(List<PDDocument> documents) throws IOException { private PDDocument mergeDocuments(List<PDDocument> documents) throws IOException {
PDDocument mergedDoc = new PDDocument(); PDDocument mergedDoc = new PDDocument();
for (PDDocument doc : documents) { for (PDDocument doc : documents) {
for (PDPage page : doc.getPages()) { for (PDPage page : doc.getPages()) {
@@ -86,8 +84,8 @@ public class MergeController {
}; };
case "byPDFTitle": case "byPDFTitle":
return (file1, file2) -> { return (file1, file2) -> {
try (PDDocument doc1 = Loader.loadPDF(file1.getBytes()); try (PDDocument doc1 = PDDocument.load(file1.getInputStream());
PDDocument doc2 = Loader.loadPDF(file2.getBytes())) { PDDocument doc2 = PDDocument.load(file2.getInputStream())) {
String title1 = doc1.getDocumentInformation().getTitle(); String title1 = doc1.getDocumentInformation().getTitle();
String title2 = doc2.getDocumentInformation().getTitle(); String title2 = doc2.getDocumentInformation().getTitle();
return title1.compareTo(title2); return title1.compareTo(title2);
@@ -108,7 +106,6 @@ public class MergeController {
"This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO") "This endpoint merges multiple PDF files into a single PDF file. The merged file will contain all pages from the input files in the order they were provided. Input:PDF Output:PDF Type:MISO")
public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form) public ResponseEntity<byte[]> mergePdfs(@ModelAttribute MergePdfsRequest form)
throws IOException { throws IOException {
List<File> filesToDelete = new ArrayList<File>();
try { try {
MultipartFile[] files = form.getFileInput(); MultipartFile[] files = form.getFileInput();
Arrays.sort(files, getSortComparator(form.getSortType())); Arrays.sort(files, getSortComparator(form.getSortType()));
@@ -116,27 +113,20 @@ public class MergeController {
PDFMergerUtility mergedDoc = new PDFMergerUtility(); PDFMergerUtility mergedDoc = new PDFMergerUtility();
ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream(); ByteArrayOutputStream docOutputstream = new ByteArrayOutputStream();
for (MultipartFile multipartFile : files) { for (MultipartFile file : files) {
File tempFile = GeneralUtils.convertMultipartFileToFile(multipartFile); mergedDoc.addSource(new ByteArrayInputStream(file.getBytes()));
filesToDelete.add(tempFile);
mergedDoc.addSource(tempFile);
} }
mergedDoc.setDestinationFileName( mergedDoc.setDestinationFileName(
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf"); files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_merged.pdf");
mergedDoc.setDestinationStream(docOutputstream); mergedDoc.setDestinationStream(docOutputstream);
mergedDoc.mergeDocuments(MemoryUsageSetting.setupMainMemoryOnly());
mergedDoc.mergeDocuments(null);
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
docOutputstream.toByteArray(), mergedDoc.getDestinationFileName()); docOutputstream.toByteArray(), mergedDoc.getDestinationFileName());
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Error in merge pdf process", ex); logger.error("Error in merge pdf process", ex);
throw ex; throw ex;
} finally {
for (File file : filesToDelete) {
file.delete();
}
} }
} }
} }

View File

@@ -4,7 +4,6 @@ import java.awt.Color;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.LayerUtility; import org.apache.pdfbox.multipdf.LayerUtility;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
@@ -21,7 +20,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -59,7 +57,7 @@ public class MultiPageLayoutController {
: (int) Math.sqrt(pagesPerSheet); : (int) Math.sqrt(pagesPerSheet);
int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet); int rows = pagesPerSheet == 2 || pagesPerSheet == 3 ? 1 : (int) Math.sqrt(pagesPerSheet);
PDDocument sourceDocument = Loader.loadPDF(file.getBytes()); PDDocument sourceDocument = PDDocument.load(file.getInputStream());
PDDocument newDocument = new PDDocument(); PDDocument newDocument = new PDDocument();
PDPage newPage = new PDPage(PDRectangle.A4); PDPage newPage = new PDPage(PDRectangle.A4);
newDocument.addPage(newPage); newDocument.addPage(newPage);
@@ -137,7 +135,6 @@ public class MultiPageLayoutController {
byte[] result = baos.toByteArray(); byte[] result = baos.toByteArray();
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
result, result,
Filenames.toSimpleFileName(file.getOriginalFilename()).replaceFirst("[.][^.]+$", "") file.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_layoutChanged.pdf");
+ "_layoutChanged.pdf");
} }
} }

View File

@@ -3,13 +3,11 @@ package stirling.software.SPDF.controller.api;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.multipdf.Overlay; import org.apache.pdfbox.multipdf.Overlay;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -20,7 +18,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -56,7 +53,7 @@ public class PdfOverlayController {
// "FixedRepeatOverlay" // "FixedRepeatOverlay"
int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode int[] counts = request.getCounts(); // Used for FixedRepeatOverlay mode
try (PDDocument basePdf = Loader.loadPDF(baseFile.getBytes()); try (PDDocument basePdf = PDDocument.load(baseFile.getInputStream());
Overlay overlay = new Overlay()) { Overlay overlay = new Overlay()) {
Map<Integer, String> overlayGuide = Map<Integer, String> overlayGuide =
prepareOverlayGuide( prepareOverlayGuide(
@@ -77,8 +74,7 @@ public class PdfOverlayController {
overlay.overlay(overlayGuide).save(outputStream); overlay.overlay(overlayGuide).save(outputStream);
byte[] data = outputStream.toByteArray(); byte[] data = outputStream.toByteArray();
String outputFilename = String outputFilename =
Filenames.toSimpleFileName(baseFile.getOriginalFilename()) baseFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
.replaceFirst("[.][^.]+$", "")
+ "_overlayed.pdf"; // Remove file extension and append .pdf + "_overlayed.pdf"; // Remove file extension and append .pdf
return WebResponseUtils.bytesToWebResponse( return WebResponseUtils.bytesToWebResponse(
@@ -135,10 +131,10 @@ public class PdfOverlayController {
overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length; overlayFileIndex = (overlayFileIndex + 1) % overlayFiles.length;
} }
try (PDDocument overlayPdf = Loader.loadPDF(overlayFiles[overlayFileIndex])) { try (PDDocument overlayPdf = PDDocument.load(overlayFiles[overlayFileIndex])) {
PDDocument singlePageDocument = new PDDocument(); PDDocument singlePageDocument = new PDDocument();
singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay)); singlePageDocument.addPage(overlayPdf.getPage(pageCountInCurrentOverlay));
File tempFile = Files.createTempFile("overlay-page-", ".pdf").toFile(); File tempFile = File.createTempFile("overlay-page-", ".pdf");
singlePageDocument.save(tempFile); singlePageDocument.save(tempFile);
singlePageDocument.close(); singlePageDocument.close();
@@ -151,7 +147,7 @@ public class PdfOverlayController {
} }
private int getNumberOfPages(File file) throws IOException { private int getNumberOfPages(File file) throws IOException {
try (PDDocument doc = Loader.loadPDF(file)) { try (PDDocument doc = PDDocument.load(file)) {
return doc.getNumberOfPages(); return doc.getNumberOfPages();
} }
} }
@@ -163,7 +159,7 @@ public class PdfOverlayController {
File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length]; File overlayFile = overlayFiles[(basePageIndex - 1) % overlayFiles.length];
// Load the overlay document to check its page count // Load the overlay document to check its page count
try (PDDocument overlayPdf = Loader.loadPDF(overlayFile)) { try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages(); int overlayPageCount = overlayPdf.getNumberOfPages();
if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) { if ((basePageIndex - 1) % overlayPageCount < overlayPageCount) {
overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath()); overlayGuide.put(basePageIndex, overlayFile.getAbsolutePath());
@@ -185,7 +181,7 @@ public class PdfOverlayController {
int repeatCount = counts[i]; int repeatCount = counts[i];
// Load the overlay document to check its page count // Load the overlay document to check its page count
try (PDDocument overlayPdf = Loader.loadPDF(overlayFile)) { try (PDDocument overlayPdf = PDDocument.load(overlayFile)) {
int overlayPageCount = overlayPdf.getNumberOfPages(); int overlayPageCount = overlayPdf.getNumberOfPages();
for (int j = 0; j < repeatCount; j++) { for (int j = 0; j < repeatCount; j++) {
for (int page = 0; page < overlayPageCount; page++) { for (int page = 0; page < overlayPageCount; page++) {

View File

@@ -2,10 +2,8 @@ package stirling.software.SPDF.controller.api;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -17,7 +15,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.github.pixee.security.Filenames;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -45,15 +42,13 @@ public class RearrangePagesPDFController {
MultipartFile pdfFile = request.getFileInput(); MultipartFile pdfFile = request.getFileInput();
String pagesToDelete = request.getPageNumbers(); String pagesToDelete = request.getPageNumbers();
PDDocument document = Loader.loadPDF(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getBytes());
// Split the page order string into an array of page numbers or range of numbers // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pagesToDelete.split(","); String[] pageOrderArr = pagesToDelete.split(",");
List<Integer> pagesToRemove = List<Integer> pagesToRemove =
GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages(), false); GeneralUtils.parsePageList(pageOrderArr, document.getNumberOfPages());
Collections.sort(pagesToRemove);
for (int i = pagesToRemove.size() - 1; i >= 0; i--) { for (int i = pagesToRemove.size() - 1; i >= 0; i--) {
int pageIndex = pagesToRemove.get(i); int pageIndex = pagesToRemove.get(i);
@@ -61,9 +56,7 @@ public class RearrangePagesPDFController {
} }
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_removed_pages.pdf");
.replaceFirst("[.][^.]+$", "")
+ "_removed_pages.pdf");
} }
private List<Integer> removeFirst(int totalPages) { private List<Integer> removeFirst(int totalPages) {
@@ -186,7 +179,7 @@ public class RearrangePagesPDFController {
String sortType = request.getCustomMode(); String sortType = request.getCustomMode();
try { try {
// Load the input PDF // Load the input PDF
PDDocument document = Loader.loadPDF(pdfFile.getBytes()); PDDocument document = PDDocument.load(pdfFile.getInputStream());
// Split the page order string into an array of page numbers or range of numbers // Split the page order string into an array of page numbers or range of numbers
String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0]; String[] pageOrderArr = pageOrder != null ? pageOrder.split(",") : new String[0];
@@ -195,7 +188,7 @@ public class RearrangePagesPDFController {
if (sortType != null && sortType.length() > 0) { if (sortType != null && sortType.length() > 0) {
newPageOrder = processSortTypes(sortType, totalPages); newPageOrder = processSortTypes(sortType, totalPages);
} else { } else {
newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages, false); newPageOrder = GeneralUtils.parsePageList(pageOrderArr, totalPages);
} }
logger.info("newPageOrder = " + newPageOrder); logger.info("newPageOrder = " + newPageOrder);
logger.info("totalPages = " + totalPages); logger.info("totalPages = " + totalPages);
@@ -217,8 +210,7 @@ public class RearrangePagesPDFController {
return WebResponseUtils.pdfDocToWebResponse( return WebResponseUtils.pdfDocToWebResponse(
document, document,
Filenames.toSimpleFileName(pdfFile.getOriginalFilename()) pdfFile.getOriginalFilename().replaceFirst("[.][^.]+$", "")
.replaceFirst("[.][^.]+$", "")
+ "_rearranged.pdf"); + "_rearranged.pdf");
} catch (IOException e) { } catch (IOException e) {
logger.error("Failed rearranging documents", e); logger.error("Failed rearranging documents", e);

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