mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-09-08 17:51:20 +02:00
Merge branch 'tempFileCleanup' of git@github.com:Stirling-Tools/Stirling-PDF.git into tempFileCleanup
This commit is contained in:
commit
f414d8afeb
@ -1,6 +1,7 @@
|
|||||||
root = true
|
root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
|
charset = utf-8
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
@ -26,6 +27,26 @@ trim_trailing_whitespace = false
|
|||||||
[*.js]
|
[*.js]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.css]
|
||||||
|
# CSS files typically use an indent size of 2 spaces for better readability and alignment with community standards.
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
[*.yaml]
|
[*.yaml]
|
||||||
|
# YAML files use an indent size of 2 spaces to maintain consistency with common YAML formatting practices.
|
||||||
|
indent_size = 2
|
||||||
insert_final_newline = false
|
insert_final_newline = false
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
# YML files follow the same conventions as YAML files, using an indent size of 2 spaces.
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = false
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
# JSON files use an indent size of 2 spaces, which is the standard for JSON formatting.
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.jsonc]
|
||||||
|
# JSONC (JSON with comments) files also follow the standard JSON formatting with an indent size of 2 spaces.
|
||||||
|
indent_size = 2
|
||||||
|
12
.github/config/repo_devs.json
vendored
Normal file
12
.github/config/repo_devs.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"repo_devs": [
|
||||||
|
"Frooodle",
|
||||||
|
"sf298",
|
||||||
|
"Ludy87",
|
||||||
|
"LaserKaspar",
|
||||||
|
"sbplat",
|
||||||
|
"reecebrowne",
|
||||||
|
"DarioGii",
|
||||||
|
"ConnorYoh"
|
||||||
|
]
|
||||||
|
}
|
13
.github/config/system-prompt.txt
vendored
Normal file
13
.github/config/system-prompt.txt
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
You are a professional software engineer specializing in reviewing pull request titles.
|
||||||
|
|
||||||
|
Your job is to analyze a git diff and an existing PR title, then evaluate and improve the PR title.
|
||||||
|
|
||||||
|
You must:
|
||||||
|
- Always return valid JSON
|
||||||
|
- Only return the JSON response (no Markdown, no formatting)
|
||||||
|
- Use one of these conventional commit types at the beginning of the title: build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
|
||||||
|
- Use lowercase only, no emojis, no trailing period
|
||||||
|
- Ensure the title is between 5 and 72 printable ASCII characters
|
||||||
|
- Never let spelling or grammar errors affect the rating
|
||||||
|
- If the PR title is rated 6 or higher and only contains spelling or grammar mistakes, correct it - do not rephrase it
|
||||||
|
- If the PR title is rated below 6, generate a new, better title based on the diff
|
5
.github/labeler-config-srvaroa.yml
vendored
5
.github/labeler-config-srvaroa.yml
vendored
@ -49,6 +49,7 @@ labels:
|
|||||||
- 'proprietary/src/main/resources/static/.*'
|
- 'proprietary/src/main/resources/static/.*'
|
||||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/.*'
|
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/.*'
|
||||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/.*'
|
- 'stirling-pdf/src/main/java/stirling/software/SPDF/UI/.*'
|
||||||
|
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/web/.*'
|
||||||
|
|
||||||
- label: 'Java'
|
- label: 'Java'
|
||||||
files:
|
files:
|
||||||
@ -80,6 +81,7 @@ labels:
|
|||||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/web/MetricsController.java'
|
||||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/.*'
|
- 'stirling-pdf/src/main/java/stirling/software/SPDF/controller/api/.*'
|
||||||
- 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/.*'
|
- 'stirling-pdf/src/main/java/stirling/software/SPDF/model/api/.*'
|
||||||
|
- 'proprietary/src/main/java/stirling/software/proprietary/security/controller/api/.*'
|
||||||
- 'scripts/png_to_webp.py'
|
- 'scripts/png_to_webp.py'
|
||||||
- 'split_photos.py'
|
- 'split_photos.py'
|
||||||
- '.github/workflows/swagger.yml'
|
- '.github/workflows/swagger.yml'
|
||||||
@ -97,7 +99,7 @@ labels:
|
|||||||
- 'Dockerfile'
|
- 'Dockerfile'
|
||||||
- 'Dockerfile.fat'
|
- 'Dockerfile.fat'
|
||||||
- 'Dockerfile.ultra-lite'
|
- 'Dockerfile.ultra-lite'
|
||||||
- 'exampleYmlFiles/*.yml'
|
- 'exampleYmlFiles/.*.yml'
|
||||||
- 'scripts/download-security-jar.sh'
|
- 'scripts/download-security-jar.sh'
|
||||||
- 'scripts/init.sh'
|
- 'scripts/init.sh'
|
||||||
- 'scripts/init-without-ocr.sh'
|
- 'scripts/init-without-ocr.sh'
|
||||||
@ -122,6 +124,7 @@ labels:
|
|||||||
- 'stirling-pdf/src/test/.*'
|
- 'stirling-pdf/src/test/.*'
|
||||||
- 'testing/.*'
|
- 'testing/.*'
|
||||||
- '.github/workflows/scorecards.yml'
|
- '.github/workflows/scorecards.yml'
|
||||||
|
- 'exampleYmlFiles/test_cicd.yml'
|
||||||
|
|
||||||
- label: 'Github'
|
- label: 'Github'
|
||||||
files:
|
files:
|
||||||
|
228
.github/workflows/ai_pr_title_review.yml
vendored
Normal file
228
.github/workflows/ai_pr_title_review.yml
vendored
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
name: AI - PR Title Review
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, edited]
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
permissions: # required for secure-repo hardening
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ai-title-review:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
models: read
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Harden Runner
|
||||||
|
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||||
|
with:
|
||||||
|
egress-policy: audit
|
||||||
|
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Configure Git to suppress detached HEAD warning
|
||||||
|
run: git config --global advice.detachedHead false
|
||||||
|
|
||||||
|
- name: Setup GitHub App Bot
|
||||||
|
if: github.actor != 'dependabot[bot]'
|
||||||
|
id: setup-bot
|
||||||
|
uses: ./.github/actions/setup-bot
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Check if actor is repo developer
|
||||||
|
id: actor
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.actor }}" == *"[bot]" ]]; then
|
||||||
|
echo "PR opened by a bot – skipping AI title review."
|
||||||
|
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if [ ! -f .github/config/repo_devs.json ]; then
|
||||||
|
echo "Error: .github/config/repo_devs.json not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Validate JSON and extract repo_devs
|
||||||
|
REPO_DEVS=$(jq -r '.repo_devs[]' .github/config/repo_devs.json 2>/dev/null || { echo "Error: Invalid JSON in repo_devs.json" >&2; exit 1; })
|
||||||
|
# Convert developer list into Bash array
|
||||||
|
mapfile -t DEVS_ARRAY <<< "$REPO_DEVS"
|
||||||
|
if [[ " ${DEVS_ARRAY[*]} " == *" ${{ github.actor }} "* ]]; then
|
||||||
|
echo "is_repo_dev=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "is_repo_dev=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Get PR diff
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: get_diff
|
||||||
|
run: |
|
||||||
|
git fetch origin ${{ github.base_ref }}
|
||||||
|
git diff origin/${{ github.base_ref }}...HEAD | head -n 10000 | grep -vP '[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\x{202E}\x{200B}]' > pr.diff
|
||||||
|
echo "diff<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat pr.diff >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Check and sanitize PR title
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: sanitize_pr_title
|
||||||
|
env:
|
||||||
|
PR_TITLE_RAW: ${{ github.event.pull_request.title }}
|
||||||
|
run: |
|
||||||
|
# Sanitize PR title: max 72 characters, only printable characters
|
||||||
|
PR_TITLE=$(echo "$PR_TITLE_RAW" | tr -d '\n\r' | head -c 72 | sed 's/[^[:print:]]//g')
|
||||||
|
if [[ ${#PR_TITLE} -lt 5 ]]; then
|
||||||
|
echo "PR title is too short. Must be at least 5 characters." >&2
|
||||||
|
fi
|
||||||
|
echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: AI PR Title Analysis
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
id: ai-title-analysis
|
||||||
|
uses: actions/ai-inference@d645f067d89ee1d5d736a5990e327e504d1c5a4a # v1.1.0
|
||||||
|
with:
|
||||||
|
model: openai/gpt-4o
|
||||||
|
system-prompt-file: ".github/config/system-prompt.txt"
|
||||||
|
prompt: |
|
||||||
|
Based on the following input data:
|
||||||
|
|
||||||
|
{
|
||||||
|
"diff": "${{ steps.get_diff.outputs.diff }}",
|
||||||
|
"pr_title": "${{ steps.sanitize_pr_title.outputs.pr_title }}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Respond ONLY with valid JSON in the format:
|
||||||
|
{
|
||||||
|
"improved_rating": <0-10>,
|
||||||
|
"improved_ai_title_rating": <0-10>,
|
||||||
|
"improved_title": "<ai generated title>"
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Validate and set SCRIPT_OUTPUT
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
run: |
|
||||||
|
cat <<EOF > ai_response.json
|
||||||
|
${{ steps.ai-title-analysis.outputs.response }}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Validate JSON structure
|
||||||
|
jq -e '
|
||||||
|
(keys | sort) == ["improved_ai_title_rating", "improved_rating", "improved_title"] and
|
||||||
|
(.improved_rating | type == "number" and . >= 0 and . <= 10) and
|
||||||
|
(.improved_ai_title_rating | type == "number" and . >= 0 and . <= 10) and
|
||||||
|
(.improved_title | type == "string")
|
||||||
|
' ai_response.json
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Invalid AI response format" >&2
|
||||||
|
cat ai_response.json >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Parse JSON fields
|
||||||
|
IMPROVED_RATING=$(jq -r '.improved_rating' ai_response.json)
|
||||||
|
IMPROVED_TITLE=$(jq -r '.improved_title' ai_response.json)
|
||||||
|
# Limit comment length to 1000 characters
|
||||||
|
COMMENT=$(cat <<EOF
|
||||||
|
## 🤖 AI PR Title Suggestion
|
||||||
|
|
||||||
|
**PR-Title Rating**: $IMPROVED_RATING/10
|
||||||
|
|
||||||
|
### ⬇️ Suggested Title (copy & paste):
|
||||||
|
|
||||||
|
\`\`\`
|
||||||
|
$IMPROVED_TITLE
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
---
|
||||||
|
*Generated by GitHub Models AI*
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
echo "$COMMENT" > /tmp/ai-title-comment.md
|
||||||
|
# Log input and output to the GitHub Step Summary
|
||||||
|
echo "### 🤖 AI PR Title Analysis" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "### Input PR Title" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```bash' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${{ steps.sanitize_pr_title.outputs.pr_title }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '### AI Response (raw JSON)' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||||
|
cat ai_response.json >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Post comment on PR if needed
|
||||||
|
if: steps.actor.outputs.is_repo_dev == 'true'
|
||||||
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.setup-bot.outputs.token }}
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const body = fs.readFileSync('/tmp/ai-title-comment.md', 'utf8');
|
||||||
|
const { GITHUB_REPOSITORY } = process.env;
|
||||||
|
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
||||||
|
const issue_number = context.issue.number;
|
||||||
|
|
||||||
|
const ratingMatch = body.match(/\*\*PR-Title Rating\*\*: (\d+)\/10/);
|
||||||
|
const rating = ratingMatch ? parseInt(ratingMatch[1], 10) : null;
|
||||||
|
|
||||||
|
const expectedActor = "${{ steps.setup-bot.outputs.app-slug }}[bot]";
|
||||||
|
const comments = await github.rest.issues.listComments({ owner, repo, issue_number });
|
||||||
|
|
||||||
|
const existing = comments.data.find(c =>
|
||||||
|
c.user?.login === expectedActor &&
|
||||||
|
c.body.includes("## 🤖 AI PR Title Suggestion")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (rating === null) {
|
||||||
|
console.log("No rating found in AI response – skipping.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating <= 5) {
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
comment_id: existing.id,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
console.log("Updated existing suggestion comment.");
|
||||||
|
} else {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner, repo, issue_number,
|
||||||
|
body
|
||||||
|
});
|
||||||
|
console.log("Created new suggestion comment.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const praise = `## 🤖 AI PR Title Suggestion\n\nGreat job! The current PR title is clear and well-structured.\n\n✅ No suggestions needed.\n\n---\n*Generated by GitHub Models AI*`;
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
await github.rest.issues.updateComment({
|
||||||
|
owner, repo,
|
||||||
|
comment_id: existing.id,
|
||||||
|
body: praise
|
||||||
|
});
|
||||||
|
console.log("Replaced suggestion with praise.");
|
||||||
|
} else {
|
||||||
|
console.log("Rating > 5 and no existing comment – skipping comment.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: is not repo dev
|
||||||
|
if: steps.actor.outputs.is_repo_dev != 'true'
|
||||||
|
run: |
|
||||||
|
exit 0 # Skip the AI title review for non-repo developers
|
||||||
|
|
||||||
|
- name: Clean up
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -f pr.diff ai_response.json /tmp/ai-title-comment.md
|
||||||
|
echo "Cleaned up temporary files."
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
9
.github/workflows/check_properties.yml
vendored
9
.github/workflows/check_properties.yml
vendored
@ -269,3 +269,12 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Failing the job because errors were detected."
|
echo "Failing the job because errors were detected."
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
- name: Cleanup temporary files
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
echo "Cleaning up temporary files..."
|
||||||
|
rm -rf pr-branch
|
||||||
|
rm -f pr-branch-messages_en_GB.properties main-branch-messages_en_GB.properties changed_files.txt result.txt
|
||||||
|
echo "Cleanup complete."
|
||||||
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.11
|
rev: v0.12.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args:
|
args:
|
||||||
@ -16,13 +16,13 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
args:
|
args:
|
||||||
- --ignore-words-list=
|
- --ignore-words-list=thirdParty,tabEl,tabEls
|
||||||
- --skip="./.*,*.csv,*.json,*.ambr"
|
- --skip="./.*,*.csv,*.json,*.ambr"
|
||||||
- --quiet-level=2
|
- --quiet-level=2
|
||||||
files: \.(html|css|js|py|md)$
|
files: \.(html|css|js|py|md)$
|
||||||
exclude: (.vscode|.devcontainer|stirling-pdf/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
exclude: (.vscode|.devcontainer|stirling-pdf/src/main/resources|Dockerfile|.*/pdfjs.*|.*/thirdParty.*|bootstrap.*|.*\.min\..*|.*diff\.js)
|
||||||
- repo: https://github.com/gitleaks/gitleaks
|
- repo: https://github.com/gitleaks/gitleaks
|
||||||
rev: v8.26.0
|
rev: v8.27.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: gitleaks
|
- id: gitleaks
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -15,6 +15,7 @@
|
|||||||
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
"ms-azuretools.vscode-docker", // Docker extension for Visual Studio Code
|
||||||
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
|
"GitHub.copilot", // GitHub Copilot AI pair programmer for Visual Studio Code
|
||||||
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
|
"GitHub.vscode-pull-request-github", // GitHub Pull Requests extension for Visual Studio Code
|
||||||
"charliermarsh.ruff" // Ruff code formatter for Python to follow the Ruff Style Guide
|
"charliermarsh.ruff", // Ruff code formatter for Python to follow the Ruff Style Guide
|
||||||
|
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
35
.vscode/settings.json
vendored
35
.vscode/settings.json
vendored
@ -6,6 +6,27 @@
|
|||||||
"[java]": {
|
"[java]": {
|
||||||
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
"editor.defaultFormatter": "josevseb.google-java-format-for-vs-code"
|
||||||
},
|
},
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||||
|
},
|
||||||
|
"[gradle-kotlin-dsl]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
|
"[markdown]": {
|
||||||
|
"editor.defaultFormatter": "yzhang.markdown-all-in-one"
|
||||||
|
},
|
||||||
|
"[gradle-build]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
|
"[gradle]": {
|
||||||
|
"editor.defaultFormatter": "vscjava.vscode-gradle"
|
||||||
|
},
|
||||||
"java.compile.nullAnalysis.mode": "automatic",
|
"java.compile.nullAnalysis.mode": "automatic",
|
||||||
"java.configuration.updateBuildConfiguration": "interactive",
|
"java.configuration.updateBuildConfiguration": "interactive",
|
||||||
"java.format.enabled": true,
|
"java.format.enabled": true,
|
||||||
@ -86,9 +107,21 @@
|
|||||||
"spring.initializr.defaultLanguage": "Java",
|
"spring.initializr.defaultLanguage": "Java",
|
||||||
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
"spring.initializr.defaultGroupId": "stirling.software.SPDF",
|
||||||
"spring.initializr.defaultArtifactId": "SPDF",
|
"spring.initializr.defaultArtifactId": "SPDF",
|
||||||
|
"java.jdt.ls.lombokSupport.enabled": true,
|
||||||
|
"html.format.wrapLineLength": 127,
|
||||||
|
"html.format.enable": true,
|
||||||
|
"html.format.indentInnerHtml": true,
|
||||||
|
"html.format.unformatted": "script,style,textarea",
|
||||||
|
"html.format.contentUnformatted": "pre,code",
|
||||||
|
"html.format.extraLiners": "head,body,/html",
|
||||||
|
"html.format.wrapAttributes": "force",
|
||||||
|
"html.format.wrapAttributesIndentSize": 2,
|
||||||
|
"html.format.indentHandlebars": true,
|
||||||
|
"html.format.preserveNewLines": true,
|
||||||
|
"html.format.maxPreserveNewLines": 2,
|
||||||
"java.project.sourcePaths": [
|
"java.project.sourcePaths": [
|
||||||
"stirling-pdf/src/main/java",
|
"stirling-pdf/src/main/java",
|
||||||
"common/src/main/java",
|
"common/src/main/java",
|
||||||
"proprietary/src/main/java"
|
"proprietary/src/main/java"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ spotless {
|
|||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
api 'org.springframework.boot:spring-boot-starter-web'
|
api 'org.springframework.boot:spring-boot-starter-web'
|
||||||
|
api 'org.springframework.boot:spring-boot-starter-aop'
|
||||||
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
api 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
api 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20240325.1'
|
||||||
api 'com.fathzer:javaluator:3.0.6'
|
api 'com.fathzer:javaluator:3.0.6'
|
||||||
@ -28,5 +29,5 @@ dependencies {
|
|||||||
api 'org.snakeyaml:snakeyaml-engine:2.9'
|
api 'org.snakeyaml:snakeyaml-engine:2.9'
|
||||||
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
api "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.9"
|
||||||
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
api 'jakarta.mail:jakarta.mail-api:2.1.3'
|
||||||
api 'org.springframework.boot:spring-boot-starter-aop'
|
runtimeOnly 'org.eclipse.angus:angus-mail:2.0.3'
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.cos.COSDictionary;
|
||||||
|
import org.apache.pdfbox.cos.COSName;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.PageMode;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class AttachmentUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the PDF catalog viewer preferences to display attachments in the viewer.
|
||||||
|
*
|
||||||
|
* @param document The <code>PDDocument</code> to modify.
|
||||||
|
* @param pageMode The <code>PageMode</code> to set for the PDF viewer. <code>PageMode</code>
|
||||||
|
* values: <code>UseNone</code>, <code>UseOutlines</code>, <code>UseThumbs</code>, <code>
|
||||||
|
* FullScreen</code>, <code>UseOC</code>, <code>UseAttachments</code>.
|
||||||
|
*/
|
||||||
|
public static void setCatalogViewerPreferences(PDDocument document, PageMode pageMode) {
|
||||||
|
try {
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
if (catalog != null) {
|
||||||
|
COSDictionary catalogDict = catalog.getCOSObject();
|
||||||
|
|
||||||
|
catalog.setPageMode(pageMode);
|
||||||
|
catalogDict.setName(COSName.PAGE_MODE, pageMode.stringValue());
|
||||||
|
|
||||||
|
COSDictionary viewerPrefs =
|
||||||
|
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
|
||||||
|
if (viewerPrefs == null) {
|
||||||
|
viewerPrefs = new COSDictionary();
|
||||||
|
catalogDict.setItem(COSName.VIEWER_PREFERENCES, viewerPrefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewerPrefs.setName(
|
||||||
|
COSName.getPDFName("NonFullScreenPageMode"), pageMode.stringValue());
|
||||||
|
|
||||||
|
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to set catalog viewer preferences for attachments", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package stirling.software.common.util;
|
package stirling.software.common.util;
|
||||||
|
|
||||||
|
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -20,13 +22,11 @@ import java.util.Properties;
|
|||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.apache.pdfbox.cos.COSDictionary;
|
|
||||||
import org.apache.pdfbox.cos.COSName;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
|
||||||
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||||
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||||
import org.apache.pdfbox.pdmodel.PDPage;
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PageMode;
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||||
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||||
@ -42,10 +42,13 @@ import lombok.experimental.UtilityClass;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
import stirling.software.common.model.api.converters.EmlToPdfRequest;
|
||||||
|
import stirling.software.common.model.api.converters.HTMLToPdfRequest;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@UtilityClass
|
@UtilityClass
|
||||||
public class EmlToPdf {
|
public class EmlToPdf {
|
||||||
|
|
||||||
private static final class StyleConstants {
|
private static final class StyleConstants {
|
||||||
// Font and layout constants
|
// Font and layout constants
|
||||||
static final int DEFAULT_FONT_SIZE = 12;
|
static final int DEFAULT_FONT_SIZE = 12;
|
||||||
@ -197,8 +200,7 @@ public class EmlToPdf {
|
|||||||
TempFileManager tempFileManager)
|
TempFileManager tempFileManager)
|
||||||
throws IOException, InterruptedException {
|
throws IOException, InterruptedException {
|
||||||
|
|
||||||
stirling.software.common.model.api.converters.HTMLToPdfRequest htmlRequest =
|
HTMLToPdfRequest htmlRequest = createHtmlRequest(request);
|
||||||
createHtmlRequest(request);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return FileToPdf.convertHtmlToPdf(
|
return FileToPdf.convertHtmlToPdf(
|
||||||
@ -884,33 +886,33 @@ public class EmlToPdf {
|
|||||||
Class<?> messageClass = message.getClass();
|
Class<?> messageClass = message.getClass();
|
||||||
|
|
||||||
// Extract headers via reflection
|
// Extract headers via reflection
|
||||||
java.lang.reflect.Method getSubject = messageClass.getMethod("getSubject");
|
Method getSubject = messageClass.getMethod("getSubject");
|
||||||
String subject = (String) getSubject.invoke(message);
|
String subject = (String) getSubject.invoke(message);
|
||||||
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
|
content.setSubject(subject != null ? safeMimeDecode(subject) : "No Subject");
|
||||||
|
|
||||||
java.lang.reflect.Method getFrom = messageClass.getMethod("getFrom");
|
Method getFrom = messageClass.getMethod("getFrom");
|
||||||
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
|
Object[] fromAddresses = (Object[]) getFrom.invoke(message);
|
||||||
content.setFrom(
|
content.setFrom(
|
||||||
fromAddresses != null && fromAddresses.length > 0
|
fromAddresses != null && fromAddresses.length > 0
|
||||||
? safeMimeDecode(fromAddresses[0].toString())
|
? safeMimeDecode(fromAddresses[0].toString())
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
java.lang.reflect.Method getAllRecipients = messageClass.getMethod("getAllRecipients");
|
Method getAllRecipients = messageClass.getMethod("getAllRecipients");
|
||||||
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
|
Object[] recipients = (Object[]) getAllRecipients.invoke(message);
|
||||||
content.setTo(
|
content.setTo(
|
||||||
recipients != null && recipients.length > 0
|
recipients != null && recipients.length > 0
|
||||||
? safeMimeDecode(recipients[0].toString())
|
? safeMimeDecode(recipients[0].toString())
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
java.lang.reflect.Method getSentDate = messageClass.getMethod("getSentDate");
|
Method getSentDate = messageClass.getMethod("getSentDate");
|
||||||
content.setDate((Date) getSentDate.invoke(message));
|
content.setDate((Date) getSentDate.invoke(message));
|
||||||
|
|
||||||
// Extract content
|
// Extract content
|
||||||
java.lang.reflect.Method getContent = messageClass.getMethod("getContent");
|
Method getContent = messageClass.getMethod("getContent");
|
||||||
Object messageContent = getContent.invoke(message);
|
Object messageContent = getContent.invoke(message);
|
||||||
|
|
||||||
if (messageContent instanceof String stringContent) {
|
if (messageContent instanceof String stringContent) {
|
||||||
java.lang.reflect.Method getContentType = messageClass.getMethod("getContentType");
|
Method getContentType = messageClass.getMethod("getContentType");
|
||||||
String contentType = (String) getContentType.invoke(message);
|
String contentType = (String) getContentType.invoke(message);
|
||||||
if (contentType != null && contentType.toLowerCase().contains("text/html")) {
|
if (contentType != null && contentType.toLowerCase().contains("text/html")) {
|
||||||
content.setHtmlBody(stringContent);
|
content.setHtmlBody(stringContent);
|
||||||
@ -949,11 +951,10 @@ public class EmlToPdf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Class<?> multipartClass = multipart.getClass();
|
Class<?> multipartClass = multipart.getClass();
|
||||||
java.lang.reflect.Method getCount = multipartClass.getMethod("getCount");
|
Method getCount = multipartClass.getMethod("getCount");
|
||||||
int count = (Integer) getCount.invoke(multipart);
|
int count = (Integer) getCount.invoke(multipart);
|
||||||
|
|
||||||
java.lang.reflect.Method getBodyPart =
|
Method getBodyPart = multipartClass.getMethod("getBodyPart", int.class);
|
||||||
multipartClass.getMethod("getBodyPart", int.class);
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
Object part = getBodyPart.invoke(multipart, i);
|
Object part = getBodyPart.invoke(multipart, i);
|
||||||
@ -974,12 +975,12 @@ public class EmlToPdf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Class<?> partClass = part.getClass();
|
Class<?> partClass = part.getClass();
|
||||||
java.lang.reflect.Method isMimeType = partClass.getMethod("isMimeType", String.class);
|
Method isMimeType = partClass.getMethod("isMimeType", String.class);
|
||||||
java.lang.reflect.Method getContent = partClass.getMethod("getContent");
|
Method getContent = partClass.getMethod("getContent");
|
||||||
java.lang.reflect.Method getDisposition = partClass.getMethod("getDisposition");
|
Method getDisposition = partClass.getMethod("getDisposition");
|
||||||
java.lang.reflect.Method getFileName = partClass.getMethod("getFileName");
|
Method getFileName = partClass.getMethod("getFileName");
|
||||||
java.lang.reflect.Method getContentType = partClass.getMethod("getContentType");
|
Method getContentType = partClass.getMethod("getContentType");
|
||||||
java.lang.reflect.Method getHeader = partClass.getMethod("getHeader", String.class);
|
Method getHeader = partClass.getMethod("getHeader", String.class);
|
||||||
|
|
||||||
Object disposition = getDisposition.invoke(part);
|
Object disposition = getDisposition.invoke(part);
|
||||||
String filename = (String) getFileName.invoke(part);
|
String filename = (String) getFileName.invoke(part);
|
||||||
@ -1186,7 +1187,7 @@ public class EmlToPdf {
|
|||||||
private static byte[] attachFilesToPdf(
|
private static byte[] attachFilesToPdf(
|
||||||
byte[] pdfBytes,
|
byte[] pdfBytes,
|
||||||
List<EmailAttachment> attachments,
|
List<EmailAttachment> attachments,
|
||||||
stirling.software.common.service.CustomPDFDocumentFactory pdfDocumentFactory)
|
CustomPDFDocumentFactory pdfDocumentFactory)
|
||||||
throws IOException {
|
throws IOException {
|
||||||
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
try (PDDocument document = pdfDocumentFactory.load(pdfBytes);
|
||||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||||
@ -1244,15 +1245,13 @@ public class EmlToPdf {
|
|||||||
document, new ByteArrayInputStream(attachment.getData()));
|
document, new ByteArrayInputStream(attachment.getData()));
|
||||||
embeddedFile.setSize(attachment.getData().length);
|
embeddedFile.setSize(attachment.getData().length);
|
||||||
embeddedFile.setCreationDate(new GregorianCalendar());
|
embeddedFile.setCreationDate(new GregorianCalendar());
|
||||||
if (attachment.getContentType() != null) {
|
|
||||||
embeddedFile.setSubtype(attachment.getContentType());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create file specification
|
// Create file specification
|
||||||
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
|
PDComplexFileSpecification fileSpec = new PDComplexFileSpecification();
|
||||||
fileSpec.setFile(uniqueFilename);
|
fileSpec.setFile(uniqueFilename);
|
||||||
fileSpec.setEmbeddedFile(embeddedFile);
|
fileSpec.setEmbeddedFile(embeddedFile);
|
||||||
if (attachment.getContentType() != null) {
|
if (attachment.getContentType() != null) {
|
||||||
|
embeddedFile.setSubtype(attachment.getContentType());
|
||||||
fileSpec.setFileDescription("Email attachment: " + uniqueFilename);
|
fileSpec.setFileDescription("Email attachment: " + uniqueFilename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1274,7 +1273,7 @@ public class EmlToPdf {
|
|||||||
efTree.setNames(efMap);
|
efTree.setNames(efMap);
|
||||||
|
|
||||||
// Set catalog viewer preferences to automatically show attachments pane
|
// Set catalog viewer preferences to automatically show attachments pane
|
||||||
setCatalogViewerPreferences(document);
|
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add attachment annotations to the first page for each embedded file
|
// Add attachment annotations to the first page for each embedded file
|
||||||
@ -1428,41 +1427,7 @@ public class EmlToPdf {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setCatalogViewerPreferences(PDDocument document) {
|
// MIME header decoding functionality for RFC 2047 encoded headers - moved to constants
|
||||||
try {
|
|
||||||
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
|
||||||
if (catalog != null) {
|
|
||||||
// Get the catalog's COS dictionary to work with low-level PDF objects
|
|
||||||
COSDictionary catalogDict = catalog.getCOSObject();
|
|
||||||
|
|
||||||
// Set PageMode to UseAttachments - this is the standard PDF specification approach
|
|
||||||
// PageMode values: UseNone, UseOutlines, UseThumbs, FullScreen, UseOC,
|
|
||||||
// UseAttachments
|
|
||||||
catalogDict.setName(COSName.PAGE_MODE, "UseAttachments");
|
|
||||||
|
|
||||||
// Also set viewer preferences for better attachment viewing experience
|
|
||||||
COSDictionary viewerPrefs =
|
|
||||||
(COSDictionary) catalogDict.getDictionaryObject(COSName.VIEWER_PREFERENCES);
|
|
||||||
if (viewerPrefs == null) {
|
|
||||||
viewerPrefs = new COSDictionary();
|
|
||||||
catalogDict.setItem(COSName.VIEWER_PREFERENCES, viewerPrefs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set NonFullScreenPageMode to UseAttachments as fallback for viewers that support
|
|
||||||
// it
|
|
||||||
viewerPrefs.setName(COSName.getPDFName("NonFullScreenPageMode"), "UseAttachments");
|
|
||||||
|
|
||||||
// Additional viewer preferences that may help with attachment display
|
|
||||||
viewerPrefs.setBoolean(COSName.getPDFName("DisplayDocTitle"), true);
|
|
||||||
|
|
||||||
log.info(
|
|
||||||
"Set PDF PageMode to UseAttachments to automatically show attachments pane");
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Log warning but don't fail the entire operation for viewer preferences
|
|
||||||
log.warn("Failed to set catalog viewer preferences for attachments", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String decodeMimeHeader(String encodedText) {
|
private static String decodeMimeHeader(String encodedText) {
|
||||||
if (encodedText == null || encodedText.trim().isEmpty()) {
|
if (encodedText == null || encodedText.trim().isEmpty()) {
|
||||||
|
@ -16,12 +16,12 @@ import io.github.pixee.security.Filenames;
|
|||||||
|
|
||||||
public class WebResponseUtils {
|
public class WebResponseUtils {
|
||||||
|
|
||||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||||
ByteArrayOutputStream baos, String docName) throws IOException {
|
ByteArrayOutputStream baos, String docName) throws IOException {
|
||||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
|
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResponseEntity<byte[]> boasToWebResponse(
|
public static ResponseEntity<byte[]> baosToWebResponse(
|
||||||
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
|
ByteArrayOutputStream baos, String docName, MediaType mediaType) throws IOException {
|
||||||
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
|
return WebResponseUtils.bytesToWebResponse(baos.toByteArray(), docName, mediaType);
|
||||||
}
|
}
|
||||||
@ -44,8 +44,7 @@ public class WebResponseUtils {
|
|||||||
headers.setContentType(mediaType);
|
headers.setContentType(mediaType);
|
||||||
headers.setContentLength(bytes.length);
|
headers.setContentLength(bytes.length);
|
||||||
String encodedDocName =
|
String encodedDocName =
|
||||||
URLEncoder.encode(docName, StandardCharsets.UTF_8.toString())
|
URLEncoder.encode(docName, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||||
.replaceAll("\\+", "%20");
|
|
||||||
headers.setContentDispositionFormData("attachment", encodedDocName);
|
headers.setContentDispositionFormData("attachment", encodedDocName);
|
||||||
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
|
||||||
}
|
}
|
||||||
@ -61,9 +60,8 @@ public class WebResponseUtils {
|
|||||||
// Open Byte Array and save document to it
|
// Open Byte Array and save document to it
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
document.save(baos);
|
document.save(baos);
|
||||||
// Close the document
|
|
||||||
document.close();
|
document.close();
|
||||||
|
|
||||||
return boasToWebResponse(baos, docName);
|
return baosToWebResponse(baos, docName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ public class WebResponseUtilsTest {
|
|||||||
String docName = "sample.pdf";
|
String docName = "sample.pdf";
|
||||||
|
|
||||||
ResponseEntity<byte[]> responseEntity =
|
ResponseEntity<byte[]> responseEntity =
|
||||||
WebResponseUtils.boasToWebResponse(baos, docName);
|
WebResponseUtils.baosToWebResponse(baos, docName);
|
||||||
|
|
||||||
assertNotNull(responseEntity);
|
assertNotNull(responseEntity);
|
||||||
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
|
||||||
|
@ -174,6 +174,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Other", "get-info-on-pdf");
|
addEndpointToGroup("Other", "get-info-on-pdf");
|
||||||
addEndpointToGroup("Other", "show-javascript");
|
addEndpointToGroup("Other", "show-javascript");
|
||||||
addEndpointToGroup("Other", "remove-image-pdf");
|
addEndpointToGroup("Other", "remove-image-pdf");
|
||||||
|
addEndpointToGroup("Other", "add-attachments");
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
addEndpointToGroup("CLI", "compress-pdf");
|
addEndpointToGroup("CLI", "compress-pdf");
|
||||||
@ -252,6 +253,7 @@ public class EndpointConfiguration {
|
|||||||
addEndpointToGroup("Java", "pdf-to-text");
|
addEndpointToGroup("Java", "pdf-to-text");
|
||||||
addEndpointToGroup("Java", "remove-image-pdf");
|
addEndpointToGroup("Java", "remove-image-pdf");
|
||||||
addEndpointToGroup("Java", "pdf-to-markdown");
|
addEndpointToGroup("Java", "pdf-to-markdown");
|
||||||
|
addEndpointToGroup("Java", "add-attachments");
|
||||||
|
|
||||||
// Javascript
|
// Javascript
|
||||||
addEndpointToGroup("Javascript", "pdf-organizer");
|
addEndpointToGroup("Javascript", "pdf-organizer");
|
||||||
|
@ -225,7 +225,7 @@ public class MergeController {
|
|||||||
String mergedFileName =
|
String mergedFileName =
|
||||||
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
files[0].getOriginalFilename().replaceFirst("[.][^.]+$", "")
|
||||||
+ "_merged_unsigned.pdf";
|
+ "_merged_unsigned.pdf";
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.baosToWebResponse(
|
||||||
baos, mergedFileName); // Return the modified PDF
|
baos, mergedFileName); // Return the modified PDF
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.github.pixee.security.Filenames;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||||
|
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api/v1/misc")
|
||||||
|
@Tag(name = "Misc", description = "Miscellaneous APIs")
|
||||||
|
public class AttachmentController {
|
||||||
|
|
||||||
|
private final CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
private final AttachmentServiceInterface pdfAttachmentService;
|
||||||
|
|
||||||
|
@PostMapping(consumes = "multipart/form-data", value = "/add-attachments")
|
||||||
|
@Operation(
|
||||||
|
summary = "Add attachments to PDF",
|
||||||
|
description =
|
||||||
|
"This endpoint adds attachments to a PDF. Input:PDF, Output:PDF Type:MISO")
|
||||||
|
public ResponseEntity<byte[]> addAttachments(@ModelAttribute AddAttachmentRequest request)
|
||||||
|
throws IOException {
|
||||||
|
MultipartFile fileInput = request.getFileInput();
|
||||||
|
List<MultipartFile> attachments = request.getAttachments();
|
||||||
|
|
||||||
|
PDDocument document =
|
||||||
|
pdfAttachmentService.addAttachment(
|
||||||
|
pdfDocumentFactory.load(fileInput, false), attachments);
|
||||||
|
|
||||||
|
return WebResponseUtils.pdfDocToWebResponse(
|
||||||
|
document,
|
||||||
|
Filenames.toSimpleFileName(fileInput.getOriginalFilename())
|
||||||
|
.replaceFirst("[.][^.]+$", "")
|
||||||
|
+ "_with_attachments.pdf");
|
||||||
|
}
|
||||||
|
}
|
@ -144,7 +144,7 @@ public class BlankPageController {
|
|||||||
zos.close();
|
zos.close();
|
||||||
|
|
||||||
log.info("Returning ZIP file: {}", filename + "_processed.zip");
|
log.info("Returning ZIP file: {}", filename + "_processed.zip");
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.baosToWebResponse(
|
||||||
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, filename + "_processed.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -148,7 +148,7 @@ public class ExtractImagesController {
|
|||||||
// Create ByteArrayResource from byte array
|
// Create ByteArrayResource from byte array
|
||||||
byte[] zipContents = baos.toByteArray();
|
byte[] zipContents = baos.toByteArray();
|
||||||
|
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.baosToWebResponse(
|
||||||
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, filename + "_extracted-images.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ public class PipelineController {
|
|||||||
}
|
}
|
||||||
zipOut.close();
|
zipOut.close();
|
||||||
log.info("Returning zipped file response...");
|
log.info("Returning zipped file response...");
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.baosToWebResponse(
|
||||||
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
baos, "output.zip", MediaType.APPLICATION_OCTET_STREAM);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error handling data: ", e);
|
log.error("Error handling data: ", e);
|
||||||
|
@ -205,7 +205,7 @@ public class CertSignController {
|
|||||||
location,
|
location,
|
||||||
reason,
|
reason,
|
||||||
showLogo);
|
showLogo);
|
||||||
return WebResponseUtils.boasToWebResponse(
|
return WebResponseUtils.baosToWebResponse(
|
||||||
baos,
|
baos,
|
||||||
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
Filenames.toSimpleFileName(pdf.getOriginalFilename()).replaceFirst("[.][^.]+$", "")
|
||||||
+ "_signed.pdf");
|
+ "_signed.pdf");
|
||||||
|
@ -191,4 +191,11 @@ public class OtherWebController {
|
|||||||
model.addAttribute("currentPage", "auto-rename");
|
model.addAttribute("currentPage", "auto-rename");
|
||||||
return "misc/auto-rename";
|
return "misc/auto-rename";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/add-attachments")
|
||||||
|
@Hidden
|
||||||
|
public String attachmentsForm(Model model) {
|
||||||
|
model.addAttribute("currentPage", "add-attachments");
|
||||||
|
return "misc/add-attachments";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package stirling.software.SPDF.model.api.misc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
import stirling.software.common.model.api.PDFFile;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AddAttachmentRequest extends PDFFile {
|
||||||
|
|
||||||
|
@Schema(
|
||||||
|
description = "The image file to be overlaid onto the PDF.",
|
||||||
|
requiredMode = Schema.RequiredMode.REQUIRED,
|
||||||
|
format = "binary")
|
||||||
|
private List<MultipartFile> attachments;
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import static stirling.software.common.util.AttachmentUtils.setCatalogViewerPreferences;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.GregorianCalendar;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
|
||||||
|
import org.apache.pdfbox.pdmodel.PageMode;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AttachmentService implements AttachmentServiceInterface {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PDDocument addAttachment(PDDocument document, List<MultipartFile> attachments)
|
||||||
|
throws IOException {
|
||||||
|
PDEmbeddedFilesNameTreeNode embeddedFilesTree = getEmbeddedFilesTree(document);
|
||||||
|
Map<String, PDComplexFileSpecification> existingNames;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Map<String, PDComplexFileSpecification> names = embeddedFilesTree.getNames();
|
||||||
|
|
||||||
|
if (names == null) {
|
||||||
|
log.debug("No existing embedded files found, creating new names map.");
|
||||||
|
existingNames = new HashMap<>();
|
||||||
|
} else {
|
||||||
|
existingNames = new HashMap<>(names);
|
||||||
|
log.debug("Embedded files: {}", existingNames.keySet());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Could not retrieve existing embedded files", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachments.forEach(
|
||||||
|
attachment -> {
|
||||||
|
String filename = attachment.getOriginalFilename();
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDEmbeddedFile embeddedFile =
|
||||||
|
new PDEmbeddedFile(document, attachment.getInputStream());
|
||||||
|
embeddedFile.setSize((int) attachment.getSize());
|
||||||
|
embeddedFile.setCreationDate(new GregorianCalendar());
|
||||||
|
embeddedFile.setModDate(new GregorianCalendar());
|
||||||
|
String contentType = attachment.getContentType();
|
||||||
|
if (StringUtils.isNotBlank(contentType)) {
|
||||||
|
embeddedFile.setSubtype(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create attachments specification and associate embedded attachment with
|
||||||
|
// file
|
||||||
|
PDComplexFileSpecification fileSpecification =
|
||||||
|
new PDComplexFileSpecification();
|
||||||
|
fileSpecification.setFile(filename);
|
||||||
|
fileSpecification.setFileUnicode(filename);
|
||||||
|
fileSpecification.setFileDescription("Embedded attachment: " + filename);
|
||||||
|
fileSpecification.setEmbeddedFile(embeddedFile);
|
||||||
|
fileSpecification.setEmbeddedFileUnicode(embeddedFile);
|
||||||
|
|
||||||
|
existingNames.put(filename, fileSpecification);
|
||||||
|
|
||||||
|
log.info("Added attachment: {} ({} bytes)", filename, attachment.getSize());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("Failed to create embedded file for attachment: {}", filename, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
embeddedFilesTree.setNames(existingNames);
|
||||||
|
setCatalogViewerPreferences(document, PageMode.USE_ATTACHMENTS);
|
||||||
|
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PDEmbeddedFilesNameTreeNode getEmbeddedFilesTree(PDDocument document) {
|
||||||
|
PDDocumentCatalog catalog = document.getDocumentCatalog();
|
||||||
|
PDDocumentNameDictionary documentNames = catalog.getNames();
|
||||||
|
|
||||||
|
if (documentNames == null) {
|
||||||
|
documentNames = new PDDocumentNameDictionary(catalog);
|
||||||
|
}
|
||||||
|
|
||||||
|
catalog.setNames(documentNames);
|
||||||
|
PDEmbeddedFilesNameTreeNode embeddedFilesTree = documentNames.getEmbeddedFiles();
|
||||||
|
|
||||||
|
if (embeddedFilesTree == null) {
|
||||||
|
embeddedFilesTree = new PDEmbeddedFilesNameTreeNode();
|
||||||
|
documentNames.setEmbeddedFiles(embeddedFilesTree);
|
||||||
|
}
|
||||||
|
return embeddedFilesTree;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
public interface AttachmentServiceInterface {
|
||||||
|
|
||||||
|
PDDocument addAttachment(PDDocument document, List<MultipartFile> attachments)
|
||||||
|
throws IOException;
|
||||||
|
}
|
@ -525,6 +525,10 @@ home.addImage.title=Add image
|
|||||||
home.addImage.desc=Adds a image onto a set location on the PDF
|
home.addImage.desc=Adds a image onto a set location on the PDF
|
||||||
addImage.tags=img,jpg,picture,photo
|
addImage.tags=img,jpg,picture,photo
|
||||||
|
|
||||||
|
home.attachments.title=Add Attachments
|
||||||
|
home.attachments.desc=Add or remove embedded files (attachments) to/from a PDF
|
||||||
|
attachments.tags=embed,attach,file,attachment,attachments
|
||||||
|
|
||||||
home.watermark.title=Add Watermark
|
home.watermark.title=Add Watermark
|
||||||
home.watermark.desc=Add a custom watermark to your PDF document.
|
home.watermark.desc=Add a custom watermark to your PDF document.
|
||||||
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
watermark.tags=Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo
|
||||||
@ -1205,6 +1209,12 @@ addImage.everyPage=Every Page?
|
|||||||
addImage.upload=Add image
|
addImage.upload=Add image
|
||||||
addImage.submit=Add image
|
addImage.submit=Add image
|
||||||
|
|
||||||
|
#attachments
|
||||||
|
attachments.title=Add Attachments
|
||||||
|
attachments.header=Add attachments
|
||||||
|
attachments.description=Allows you to add attachments to the PDF
|
||||||
|
attachments.descriptionPlaceholder=Enter a description for the attachments...
|
||||||
|
attachments.addButton=Add Attachments
|
||||||
|
|
||||||
#merge
|
#merge
|
||||||
merge.title=Merge
|
merge.title=Merge
|
||||||
@ -1594,6 +1604,7 @@ fileChooser.dragAndDropPDF=Drag & Drop PDF file
|
|||||||
fileChooser.dragAndDropImage=Drag & Drop Image file
|
fileChooser.dragAndDropImage=Drag & Drop Image file
|
||||||
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
fileChooser.hoveredDragAndDrop=Drag & Drop file(s) here
|
||||||
fileChooser.extractPDF=Extracting...
|
fileChooser.extractPDF=Extracting...
|
||||||
|
fileChooser.addAttachments=drag & drop attachments here
|
||||||
|
|
||||||
#release notes
|
#release notes
|
||||||
releases.footer=Releases
|
releases.footer=Releases
|
||||||
|
@ -6,6 +6,9 @@ class FileIconFactory {
|
|||||||
return this.createPDFIcon();
|
return this.createPDFIcon();
|
||||||
case "csv":
|
case "csv":
|
||||||
return this.createCSVIcon();
|
return this.createCSVIcon();
|
||||||
|
case "xls":
|
||||||
|
case "xlsx":
|
||||||
|
return this.createXLSXIcon();
|
||||||
case "jpe":
|
case "jpe":
|
||||||
case "jpg":
|
case "jpg":
|
||||||
case "jpeg":
|
case "jpeg":
|
||||||
@ -44,8 +47,29 @@ class FileIconFactory {
|
|||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M216-144q-30 0-51-21.5T144-216v-528q0-29 21-50.5t51-21.5h528q30 0 51 21.5t21 50.5v528q0 29-21 50.5T744-144H216Zm48-144h432L552-480 444-336l-72-96-108 144Z"/></svg>`;
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M216-144q-30 0-51-21.5T144-216v-528q0-29 21-50.5t51-21.5h528q30 0 51 21.5t21 50.5v528q0 29-21 50.5T744-144H216Zm48-144h432L552-480 444-336l-72-96-108 144Z"/></svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static createCSVIcon() {
|
||||||
|
return `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-filetype-csv" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M14 4.5V14a2 2 0 0 1-2 2h-1v-1h1a1 1 0 0 0 1-1V4.5h-2A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v9H2V2a2 2 0 0 1 2-2h5.5zM3.517 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M8.717 14.841a1.13 1.13 0 0 0 .401.823q.195.162.478.252.284.091.665.091.507 0 .859-.158.354-.158.539-.54.185-.382.185-.816 0-.335-.123-.628a1.4 1.4 0 0 0-.366-.486 1.8 1.8 0 0 0-.614-.314 2.8 2.8 0 0 0-.865-.118 2.1 2.1 0 0 0-.614.094 1.4 1.4 0 0 0-.471.264 1.1 1.1 0 0 0-.298.429.9.9 0 0 0-.103.539h.606a.4.4 0 0 1 .096-.258.5.5 0 0 1 .213-.164.6.6 0 0 1 .33-.082.7.7 0 0 1 .458.132.4.4 0 0 1 .153.372.4.4 0 0 1-.085.235.7.7 0 0 1-.25.192 1.4 1.4 0 0 1-.407.115c-.127.023-.266.05-.416.081a1.8 1.8 0 0 0-.534.187 1.2 1.2 0 0 0-.382.346 1 1 0 0 0-.138.537q0 .295.101.517M14.229 13.12v.506H11.85v-.506h1.063v-1.277H11.85v-.506h2.379v.506h-1.063z"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static createXLSXIcon() {
|
||||||
|
return `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark-excel" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.884 6.68a.5.5 0 1 0-.768.64L7.349 10l-2.233 2.68a.5.5 0 0 0 .768.64L8 10.781l2.116 2.54a.5.5 0 0 0 .768-.641L8.651 10l2.233-2.68a.5.5 0 0 0-.768-.64L8 9.219l-2.116-2.54z"/>
|
||||||
|
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
static createUnknownFileIcon() {
|
static createUnknownFileIcon() {
|
||||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor"><path d="M263.72-96Q234-96 213-117.15T192-168v-624q0-29.7 21.15-50.85Q234.3-864 264-864h312l192 192v504q0 29.7-21.16 50.85Q725.68-96 695.96-96H263.72ZM528-624h168L528-792v168Z"/></svg>`;
|
return `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="bi bi-file-earmark" viewBox="0 0 16 16">
|
||||||
|
<path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +45,8 @@ function setupFileInput(chooser) {
|
|||||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
|
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropPDF;
|
||||||
} else if (inputContainer.id === 'image-upload-input-container') {
|
} else if (inputContainer.id === 'image-upload-input-container') {
|
||||||
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropImage;
|
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.dragAndDropImage;
|
||||||
|
} else if (inputContainer.id === 'attachments-input-container') {
|
||||||
|
inputContainer.querySelector('#dragAndDrop').innerHTML = window.fileInput.addAttachments;
|
||||||
}
|
}
|
||||||
let allFiles = [];
|
let allFiles = [];
|
||||||
let overlay;
|
let overlay;
|
||||||
|
@ -268,6 +268,7 @@
|
|||||||
window.fileInput = {
|
window.fileInput = {
|
||||||
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
|
dragAndDropPDF: '[[#{fileChooser.dragAndDropPDF}]]',
|
||||||
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
|
dragAndDropImage: '[[#{fileChooser.dragAndDropImage}]]',
|
||||||
|
addAttachments: '[[#{fileChooser.addAttachments}]]',
|
||||||
extractPDF: '[[#{fileChooser.extractPDF}]]',
|
extractPDF: '[[#{fileChooser.extractPDF}]]',
|
||||||
loading: '[[#{loading}]]'
|
loading: '[[#{loading}]]'
|
||||||
};</script>
|
};</script>
|
||||||
|
@ -237,6 +237,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}">
|
th:replace="~{fragments/navbarEntry :: navbarEntry('unlock-pdf-forms', 'preview_off', 'home.unlockPDFForms.title', 'home.unlockPDFForms.desc', 'unlockPDFForms.tags', 'other')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="groupAdvanced" class="feature-group">
|
<div id="groupAdvanced" class="feature-group">
|
||||||
|
@ -290,6 +290,9 @@
|
|||||||
<div
|
<div
|
||||||
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
|
th:replace="~{fragments/card :: card(id='unlock-pdf-forms', cardTitle=#{home.unlockPDFForms.title}, cardText=#{home.unlockPDFForms.desc}, cardLink='unlock-pdf-forms', toolIcon='preview_off', tags=#{unlockPDFForms.tags}, toolGroup='other')}">
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
th:replace="~{fragments/navbarEntry :: navbarEntry('add-attachments', 'attachment', 'home.attachments.title', 'home.attachments.desc', 'attachments.tags', 'other')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
|
||||||
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<th:block th:insert="~{fragments/common :: head(title=#{attachments.title}, header=#{attachments.header})}"></th:block>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="page-container">
|
||||||
|
<div id="content-wrap">
|
||||||
|
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
||||||
|
<br><br>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-md-6 bg-card">
|
||||||
|
<div class="tool-header">
|
||||||
|
<span class="material-symbols-rounded tool-header-icon other">attachment</span>
|
||||||
|
<span class="tool-header-text" th:text="#{attachments.header}"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action="#" th:action="@{/api/v1/misc/add-attachments}" method="post" enctype="multipart/form-data">
|
||||||
|
<!-- PDF file selector -->
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='fileInput', disableMultipleFiles=true, multipleInputsForSingleRequest=false, accept='application/pdf')}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachment files selector -->
|
||||||
|
<div th:replace="~{fragments/common :: fileSelector(name='attachments', disableMultipleFiles=false, multipleInputsForSingleRequest=true, accept='*/*', inputText=#{attachments.selectFiles})}">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Submit button -->
|
||||||
|
<button type="submit" id="submitBtn" class="btn btn-primary" th:text="#{attachments.addButton}">Add Attachments</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -0,0 +1,133 @@
|
|||||||
|
package stirling.software.SPDF.controller.api.misc;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.*;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.model.api.misc.AddAttachmentRequest;
|
||||||
|
import stirling.software.SPDF.service.AttachmentServiceInterface;
|
||||||
|
import stirling.software.common.service.CustomPDFDocumentFactory;
|
||||||
|
import stirling.software.common.util.WebResponseUtils;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class AttachmentControllerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CustomPDFDocumentFactory pdfDocumentFactory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private AttachmentServiceInterface pdfAttachmentService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private AttachmentController attachmentController;
|
||||||
|
|
||||||
|
private MockMultipartFile pdfFile;
|
||||||
|
private MockMultipartFile attachment1;
|
||||||
|
private MockMultipartFile attachment2;
|
||||||
|
private AddAttachmentRequest request;
|
||||||
|
private PDDocument mockDocument;
|
||||||
|
private PDDocument modifiedMockDocument;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
pdfFile = new MockMultipartFile("fileInput", "test.pdf", "application/pdf", "PDF content".getBytes());
|
||||||
|
attachment1 = new MockMultipartFile("attachment1", "file1.txt", "text/plain", "File 1 content".getBytes());
|
||||||
|
attachment2 = new MockMultipartFile("attachment2", "file2.jpg", "image/jpeg", "Image content".getBytes());
|
||||||
|
request = new AddAttachmentRequest();
|
||||||
|
mockDocument = mock(PDDocument.class);
|
||||||
|
modifiedMockDocument = mock(PDDocument.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachments_Success() throws IOException {
|
||||||
|
List<MultipartFile> attachments = List.of(attachment1, attachment2);
|
||||||
|
request.setAttachments(attachments);
|
||||||
|
request.setFileInput(pdfFile);
|
||||||
|
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("modified PDF content".getBytes());
|
||||||
|
|
||||||
|
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||||
|
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenReturn(modifiedMockDocument);
|
||||||
|
|
||||||
|
try (MockedStatic<WebResponseUtils> mockedWebResponseUtils = mockStatic(WebResponseUtils.class)) {
|
||||||
|
mockedWebResponseUtils.when(() -> WebResponseUtils.pdfDocToWebResponse(eq(modifiedMockDocument), eq("test_with_attachments.pdf")))
|
||||||
|
.thenReturn(expectedResponse);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
|
||||||
|
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||||
|
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachments_SingleAttachment() throws IOException {
|
||||||
|
List<MultipartFile> attachments = List.of(attachment1);
|
||||||
|
request.setAttachments(attachments);
|
||||||
|
request.setFileInput(pdfFile);
|
||||||
|
ResponseEntity<byte[]> expectedResponse = ResponseEntity.ok("modified PDF content".getBytes());
|
||||||
|
|
||||||
|
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||||
|
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenReturn(modifiedMockDocument);
|
||||||
|
|
||||||
|
try (MockedStatic<WebResponseUtils> mockedWebResponseUtils = mockStatic(WebResponseUtils.class)) {
|
||||||
|
mockedWebResponseUtils.when(() -> WebResponseUtils.pdfDocToWebResponse(eq(modifiedMockDocument), eq("test_with_attachments.pdf")))
|
||||||
|
.thenReturn(expectedResponse);
|
||||||
|
|
||||||
|
ResponseEntity<byte[]> response = attachmentController.addAttachments(request);
|
||||||
|
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals(HttpStatus.OK, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||||
|
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachments_IOExceptionFromPDFLoad() throws IOException {
|
||||||
|
List<MultipartFile> attachments = List.of(attachment1);
|
||||||
|
request.setAttachments(attachments);
|
||||||
|
request.setFileInput(pdfFile);
|
||||||
|
IOException ioException = new IOException("Failed to load PDF");
|
||||||
|
|
||||||
|
when(pdfDocumentFactory.load(pdfFile, false)).thenThrow(ioException);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> attachmentController.addAttachments(request));
|
||||||
|
verify(pdfDocumentFactory).load(pdfFile, false);
|
||||||
|
verifyNoInteractions(pdfAttachmentService);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachments_IOExceptionFromAttachmentService() throws IOException {
|
||||||
|
List<MultipartFile> attachments = List.of(attachment1);
|
||||||
|
request.setAttachments(attachments);
|
||||||
|
request.setFileInput(pdfFile);
|
||||||
|
IOException ioException = new IOException("Failed to add attachment");
|
||||||
|
|
||||||
|
when(pdfDocumentFactory.load(pdfFile, false)).thenReturn(mockDocument);
|
||||||
|
when(pdfAttachmentService.addAttachment(mockDocument, attachments)).thenThrow(ioException);
|
||||||
|
|
||||||
|
assertThrows(IOException.class, () -> attachmentController.addAttachments(request));
|
||||||
|
verify(pdfAttachmentService).addAttachment(mockDocument, attachments);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
package stirling.software.SPDF.service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class AttachmentServiceTest {
|
||||||
|
|
||||||
|
private AttachmentService attachmentService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
attachmentService = new AttachmentService();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachmentToPDF() throws IOException {
|
||||||
|
try (var document = new PDDocument()) {
|
||||||
|
document.setDocumentId(100L);
|
||||||
|
var attachments = List.of(mock(MultipartFile.class));
|
||||||
|
|
||||||
|
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
|
||||||
|
when(attachments.get(0).getInputStream()).thenReturn(
|
||||||
|
new ByteArrayInputStream("Test content".getBytes()));
|
||||||
|
when(attachments.get(0).getSize()).thenReturn(12L);
|
||||||
|
when(attachments.get(0).getContentType()).thenReturn("text/plain");
|
||||||
|
|
||||||
|
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertEquals(document.getDocumentId(), result.getDocumentId());
|
||||||
|
assertNotNull(result.getDocumentCatalog().getNames());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachmentToPDF_MultipleAttachments() throws IOException {
|
||||||
|
try (var document = new PDDocument()) {
|
||||||
|
document.setDocumentId(100L);
|
||||||
|
var attachment1 = mock(MultipartFile.class);
|
||||||
|
var attachment2 = mock(MultipartFile.class);
|
||||||
|
var attachments = List.of(attachment1, attachment2);
|
||||||
|
|
||||||
|
when(attachment1.getOriginalFilename()).thenReturn("document.pdf");
|
||||||
|
when(attachment1.getInputStream()).thenReturn(
|
||||||
|
new ByteArrayInputStream("PDF content".getBytes()));
|
||||||
|
when(attachment1.getSize()).thenReturn(15L);
|
||||||
|
when(attachment1.getContentType()).thenReturn("application/pdf");
|
||||||
|
|
||||||
|
when(attachment2.getOriginalFilename()).thenReturn("image.jpg");
|
||||||
|
when(attachment2.getInputStream()).thenReturn(
|
||||||
|
new ByteArrayInputStream("Image content".getBytes()));
|
||||||
|
when(attachment2.getSize()).thenReturn(20L);
|
||||||
|
when(attachment2.getContentType()).thenReturn("image/jpeg");
|
||||||
|
|
||||||
|
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertNotNull(result.getDocumentCatalog().getNames());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachmentToPDF_WithBlankContentType() throws IOException {
|
||||||
|
try (var document = new PDDocument()) {
|
||||||
|
document.setDocumentId(100L);
|
||||||
|
var attachments = List.of(mock(MultipartFile.class));
|
||||||
|
|
||||||
|
when(attachments.get(0).getOriginalFilename()).thenReturn("image.jpg");
|
||||||
|
when(attachments.get(0).getInputStream()).thenReturn(
|
||||||
|
new ByteArrayInputStream("Image content".getBytes()));
|
||||||
|
when(attachments.get(0).getSize()).thenReturn(25L);
|
||||||
|
when(attachments.get(0).getContentType()).thenReturn("");
|
||||||
|
|
||||||
|
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertNotNull(result.getDocumentCatalog().getNames());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void addAttachmentToPDF_AttachmentInputStreamThrowsIOException() throws IOException {
|
||||||
|
try (var document = new PDDocument()) {
|
||||||
|
var attachments = List.of(mock(MultipartFile.class));
|
||||||
|
var ioException = new IOException("Failed to read attachment stream");
|
||||||
|
|
||||||
|
when(attachments.get(0).getOriginalFilename()).thenReturn("test.txt");
|
||||||
|
when(attachments.get(0).getInputStream()).thenThrow(ioException);
|
||||||
|
when(attachments.get(0).getSize()).thenReturn(10L);
|
||||||
|
|
||||||
|
PDDocument result = attachmentService.addAttachment(document, attachments);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertNotNull(result.getDocumentCatalog().getNames());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -128,7 +128,7 @@ ui:
|
|||||||
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
languages: [] # If empty, all languages are enabled. To display only German and Polish ["de_DE", "pl_PL"]. British English is always enabled.
|
||||||
|
|
||||||
endpoints: # All the possible endpoints are disabled
|
endpoints: # All the possible endpoints are disabled
|
||||||
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size] # list endpoints to disable (e.g. ['img-to-pdf', 'remove-pages'])
|
toRemove: [crop, merge-pdfs, multi-page-layout, overlay-pdfs, pdf-to-single-page, rearrange-pages, remove-image-pdf, remove-pages, rotate-pdf, scale-pages, split-by-size-or-count, split-pages, split-pdf-by-chapters, split-pdf-by-sections, add-password, add-watermark, auto-redact, cert-sign, get-info-on-pdf, redact, remove-cert-sign, remove-password, sanitize-pdf, validate-signature, file-to-pdf, html-to-pdf, img-to-pdf, markdown-to-pdf, pdf-to-csv, pdf-to-html, pdf-to-img, pdf-to-markdown, pdf-to-pdfa, pdf-to-presentation, pdf-to-text, pdf-to-word, pdf-to-xml, url-to-pdf, add-image, add-page-numbers, add-stamp, auto-rename, auto-split-pdf, compress-pdf, decompress-pdf, extract-image-scans, extract-images, flatten, ocr-pdf, remove-blanks, repair, replace-invert-pdf, show-javascript, update-metadata, filter-contains-image, filter-contains-text, filter-file-size, filter-page-count, filter-page-rotation, filter-page-size, add-attachments] # 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:
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
/api/v1/misc/add-stamp
|
/api/v1/misc/add-stamp
|
||||||
/api/v1/misc/add-page-numbers
|
/api/v1/misc/add-page-numbers
|
||||||
/api/v1/misc/add-image
|
/api/v1/misc/add-image
|
||||||
|
/api/v1/misc/add-attachments
|
||||||
/api/v1/convert/url/pdf
|
/api/v1/convert/url/pdf
|
||||||
/api/v1/convert/pdf/xml
|
/api/v1/convert/pdf/xml
|
||||||
/api/v1/convert/pdf/word
|
/api/v1/convert/pdf/word
|
||||||
|
@ -51,3 +51,4 @@
|
|||||||
/swagger-ui/index.html
|
/swagger-ui/index.html
|
||||||
/licenses
|
/licenses
|
||||||
/releases
|
/releases
|
||||||
|
/add-attachments
|
||||||
|
Loading…
Reference in New Issue
Block a user